mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
196 changed files with 2758 additions and 694 deletions
@ -1,56 +0,0 @@ |
|||
name: "Commercial License : Bug Report" |
|||
description: | |
|||
Create a report to help us improve the project. For Commercial License holders only. |
|||
Please contact help@sixlabors.com for issues requiring private support. |
|||
labels: ["commercial", "needs triage"] |
|||
body: |
|||
- type: checkboxes |
|||
attributes: |
|||
label: Prerequisites |
|||
options: |
|||
- label: I have bought a Commercial License |
|||
required: true |
|||
- label: I have written a descriptive issue title |
|||
required: true |
|||
- label: I have verified that I am running the latest version of ImageSharp |
|||
required: true |
|||
- label: I have verified if the problem exist in both `DEBUG` and `RELEASE` mode |
|||
required: true |
|||
- label: I have searched [open](https://github.com/SixLabors/ImageSharp/issues) and [closed](https://github.com/SixLabors/ImageSharp/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported |
|||
required: true |
|||
- type: input |
|||
attributes: |
|||
label: ImageSharp version |
|||
validations: |
|||
required: true |
|||
- type: input |
|||
attributes: |
|||
label: Other ImageSharp packages and versions |
|||
validations: |
|||
required: true |
|||
- type: input |
|||
attributes: |
|||
label: Environment (Operating system, version and so on) |
|||
validations: |
|||
required: true |
|||
- type: input |
|||
attributes: |
|||
label: .NET Framework version |
|||
validations: |
|||
required: true |
|||
- type: textarea |
|||
attributes: |
|||
label: Description |
|||
description: A description of the bug |
|||
validations: |
|||
required: true |
|||
- type: textarea |
|||
attributes: |
|||
label: Steps to Reproduce |
|||
description: List of steps, sample code, failing test or link to a project that reproduces the behavior. Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues. |
|||
validations: |
|||
required: true |
|||
- type: textarea |
|||
attributes: |
|||
label: Images |
|||
description: Please upload images that can be used to reproduce issues in the area below. If the file type is not supported the file can be zipped and then uploaded instead. |
|||
@ -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,19 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Processing; |
|||
|
|||
[Config(typeof(Config.MultiFramework))] |
|||
public class OilPaint |
|||
{ |
|||
[Benchmark] |
|||
public void DoOilPaint() |
|||
{ |
|||
using Image<RgbaVector> image = new Image<RgbaVector>(1920, 1200, new(127, 191, 255)); |
|||
image.Mutate(ctx => ctx.OilPaint()); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:4d88eb2e50ca9dbed0e8dfe4ad278cd88ddb9d3408b30d9dfe59102b167f570b |
|||
size 262887 |
|||
oid sha256:98115a7087aced0c28cefa32a57bc72be245886cabeefc4ff7faf7984236218c |
|||
size 271226 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:97cfbef27319988b67aeac87d469d044edd925c90e4774170465f51eed85c16a |
|||
size 42915 |
|||
oid sha256:58c03e354b108033873e2a4c0b043ce15919c4d0630e6ca72ff70b89cbedb979 |
|||
size 44239 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3a799b69938507e3fd2a74ffa7c6c6ad6574acb25861a0a50cb8361520d468de |
|||
size 41809 |
|||
oid sha256:f987f4d270568facefc11eee7f81dd156af56c26b69fe3a6d2d2e9818652befa |
|||
size 43116 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:9932db58eeb966cd293b1b7a375e9c1b17b6d09153c679ebf03d42a08d2ce9b3 |
|||
size 43332 |
|||
oid sha256:ebdad83936e50bbb00fd74b7dd7d2f5a480bb7347aa3d151e7827107cd279bac |
|||
size 44441 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:67ebf42bc82483d1778254d95a376230437611dce91c80f8ecda608de56bffe7 |
|||
size 43108 |
|||
oid sha256:ccdf5937c30999e3b09071200de2e1db63b606ad9cbf6f7677a7499fb0b52963 |
|||
size 44252 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8d5cdda990ac146a7580f58cc2bcab72f903dde564a394de7df4cc37e6dcf2dd |
|||
size 43906 |
|||
oid sha256:baf70b732646d7c6cec60cfbe569ec673418dfb2dd0b5937bccfb91d9821d586 |
|||
size 45053 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:c11e6c197bd1c227ae8f4af7e8c232cfe75db6929ab12bddf5e6554fbaed3f01 |
|||
size 50716 |
|||
oid sha256:f6a1eae610ed730e4cec41693829929ba8db674886c2bd558f1b8893d2b76802 |
|||
size 51201 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:69ff9654eb61f2bfdd44fb25aff959c5b831015e283cc91a90e3abf6f681dc88 |
|||
size 52429 |
|||
oid sha256:ba674e0236c2e146c64a7f3e224c702030769304cd0fd624d1989536da341659 |
|||
size 52814 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6ee945ac5120e4198d1f94e6467cc0f77c90869bf5a09942e7720dddcfdfbe07 |
|||
size 51262 |
|||
oid sha256:316231c8d837f864cf62dcc79fdce698dc8c45c0327372de42c2b89eac1d9f81 |
|||
size 51851 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1b023505175ae39a93fa55c85aa31466f0aca76fab0ee54f9667648b91f9aeb9 |
|||
size 50789 |
|||
oid sha256:b58144146585f50960dfd6ac5dc3f52238160287ae5f9b18c6796962cc3d2fd2 |
|||
size 51550 |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue