diff --git a/.gitattributes b/.gitattributes index 537d9cef11..435f0bb91b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -93,6 +93,7 @@ *.gif binary *.jpg binary *.png binary +*.tga binary *.ttf binary *.snk binary diff --git a/Directory.Build.targets b/Directory.Build.targets index 1dc081782a..01c1f10397 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -24,7 +24,7 @@ - + diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 7460c9cac1..c51a54a40b 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; @@ -14,6 +15,20 @@ namespace SixLabors.ImageSharp /// internal static class ImageMaths { + /// + /// Vector for converting pixel to gray value as specified by ITU-R Recommendation BT.709. + /// + private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); + + /// + /// Convert a pixel value to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from. + /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels) + => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + /// /// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709. /// diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index ae20490c77..4dba7a7e8e 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Processing; using SixLabors.Memory; @@ -150,6 +151,7 @@ public Configuration Clone() /// /// /// . + /// . /// /// The default configuration of . internal static Configuration CreateDefaultInstance() @@ -158,7 +160,8 @@ internal static Configuration CreateDefaultInstance() new PngConfigurationModule(), new JpegConfigurationModule(), new GifConfigurationModule(), - new BmpConfigurationModule()); + new BmpConfigurationModule(), + new TgaConfigurationModule()); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index b7733e0269..03e082cce0 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -387,9 +387,10 @@ private void ReadRle24(Buffer2D pixels, int width, int height, b if (rowHasUndefinedPixels) { // Slow path with undefined pixels. + int rowStartIdx = y * width * 3; for (int x = 0; x < width; x++) { - int idx = (y * width * 3) + (x * 3); + int idx = rowStartIdx + (x * 3); if (undefinedPixels[x, y]) { switch (this.options.RleSkippedPixelHandling) @@ -418,9 +419,10 @@ private void ReadRle24(Buffer2D pixels, int width, int height, b else { // Fast path without any undefined pixels. + int rowStartIdx = y * width * 3; for (int x = 0; x < width; x++) { - int idx = (y * width * 3) + (x * 3); + int idx = rowStartIdx + (x * 3); color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/README.md b/src/ImageSharp/Formats/README.md new file mode 100644 index 0000000000..4a2b401b1d --- /dev/null +++ b/src/ImageSharp/Formats/README.md @@ -0,0 +1,6 @@ +# Encoder/Decoder for true vision targa files + +Useful links for reference: + +- [FileFront](https://www.fileformat.info/format/tga/egff.htm) +- [Tga Specification](http://www.dca.fee.unicamp.br/~martino/disciplinas/ea978/tgaffs.pdf) diff --git a/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs new file mode 100644 index 0000000000..e99e8b0c8d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// The options for decoding tga images. Currently empty, but this may change in the future. + /// + internal interface ITgaDecoderOptions + { + } +} diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs new file mode 100644 index 0000000000..49983d2369 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Configuration options for use during tga encoding. + /// + internal interface ITgaEncoderOptions + { + /// + /// Gets the number of bits per pixel. + /// + TgaBitsPerPixel? BitsPerPixel { get; } + + /// + /// Gets a value indicating whether run length compression should be used. + /// + TgaCompression Compression { get; } + } +} diff --git a/src/ImageSharp/Formats/Tga/TGA_Specification.pdf b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf new file mode 100644 index 0000000000..09c9a4ddda Binary files /dev/null and b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf differ diff --git a/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs new file mode 100644 index 0000000000..a0666fa84d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Enumerates the available bits per pixel the tga encoder supports. + /// + public enum TgaBitsPerPixel : byte + { + /// + /// 8 bits per pixel. Each pixel consists of 1 byte. + /// + Pixel8 = 8, + + /// + /// 16 bits per pixel. Each pixel consists of 2 bytes. + /// + Pixel16 = 16, + + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Pixel24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes. + /// + Pixel32 = 32 + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaCompression.cs b/src/ImageSharp/Formats/Tga/TgaCompression.cs new file mode 100644 index 0000000000..cc6e005eda --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaCompression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Indicates if compression is used. + /// + public enum TgaCompression + { + /// + /// No compression is used. + /// + None, + + /// + /// Run length encoding is used. + /// + RunLength, + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs new file mode 100644 index 0000000000..18fbf4acd0 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaConfigurationModule : IConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(TgaFormat.Instance, new TgaEncoder()); + configuration.ImageFormatsManager.SetDecoder(TgaFormat.Instance, new TgaDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new TgaImageFormatDetector()); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaConstants.cs b/src/ImageSharp/Formats/Tga/TgaConstants.cs new file mode 100644 index 0000000000..5aabe92a1d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal static class TgaConstants + { + /// + /// The list of mimetypes that equate to a targa file. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/x-tga", "image/x-targa" }; + + /// + /// The list of file extensions that equate to a targa file. + /// + public static readonly IEnumerable FileExtensions = new[] { "tga", "vda", "icb", "vst" }; + + /// + /// The file header length of a tga image in bytes. + /// + public const int FileHeaderLength = 18; + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs new file mode 100644 index 0000000000..b97388773a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Image decoder for Truevision TGA images. + /// + public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector + { + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(configuration, this).Decode(stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(configuration, this).Identify(stream); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs new file mode 100644 index 0000000000..d861450e04 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -0,0 +1,588 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal sealed class TgaDecoderCore + { + /// + /// The metadata. + /// + private ImageMetadata metadata; + + /// + /// The tga specific metadata. + /// + private TgaMetadata tgaMetadata; + + /// + /// The file header containing general information about the image. + /// + private TgaFileHeader fileHeader; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The stream to decode from. + /// + private Stream currentStream; + + /// + /// The bitmap decoder options. + /// + private readonly ITgaDecoderOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. + public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.options = options; + } + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// + /// is null. + /// + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : struct, IPixel + { + try + { + bool inverted = this.ReadFileHeader(stream); + this.currentStream.Skip(this.fileHeader.IdLength); + + // Parse the color map, if present. + if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) + { + TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); + } + + if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + { + throw new UnknownImageFormatException("Width or height cannot be 0"); + } + + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + if (this.fileHeader.ColorMapType is 1) + { + if (this.fileHeader.CMapLength <= 0) + { + TgaThrowHelper.ThrowImageFormatException("Missing tga color map length"); + } + + if (this.fileHeader.CMapDepth <= 0) + { + TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); + } + + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; + using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) + { + this.currentStream.Read(palette.Array, this.fileHeader.CMapStart, colorMapSizeInBytes); + + if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) + { + this.ReadPalettedRle( + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + palette.Array, + colorMapPixelSizeInBytes, + inverted); + } + else + { + this.ReadPaletted( + this.fileHeader.Width, + this.fileHeader.Height, + pixels, + palette.Array, + colorMapPixelSizeInBytes, + inverted); + } + } + + return image; + } + + // Even if the image type indicates it is not a paletted image, it can still contain a palette. Skip those bytes. + if (this.fileHeader.CMapLength > 0) + { + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + this.currentStream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); + } + + switch (this.fileHeader.PixelDepth) + { + case 8: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); + } + else + { + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 15: + case 16: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); + } + else + { + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 24: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); + } + else + { + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + case 32: + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); + } + else + { + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); + } + + break; + + default: + TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); + break; + } + + return image; + } + catch (IndexOutOfRangeException e) + { + throw new ImageFormatException("TGA image does not have a valid format.", e); + } + } + + /// + /// Reads a uncompressed TGA image with a palette. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) + { + TPixel color = default; + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + switch (colorMapPixelSizeInBytes) + { + case 2: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + + // Set alpha value to 1, to treat it as opaque for Bgra5551. + Bgra5551 bgra = Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes]); + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + color.FromBgra5551(bgra); + pixelRow[x] = color; + } + + break; + + case 3: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 4: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + } + } + } + } + + /// + /// Reads a run length encoded TGA image with a palette. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The color palette. + /// Color map size of one entry in bytes. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) + where TPixel : struct, IPixel + { + int bytesPerPixel = 1; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) + { + TPixel color = default; + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1); + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + int rowStartIdx = y * width * bytesPerPixel; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + x; + switch (colorMapPixelSizeInBytes) + { + case 1: + color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 2: + // Set alpha value to 1, to treat it as opaque for Bgra5551. + Bgra5551 bgra = Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]); + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + color.FromBgra5551(bgra); + break; + case 3: + color.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + case 4: + color.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + break; + } + + pixelRow[x] = color; + } + } + } + } + + /// + /// Reads a uncompressed monochrome TGA image. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadMonoChrome(int width, int height, Buffer2D pixels, bool inverted) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromGray8Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 16 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadBgra16(int width, int height, Buffer2D pixels, bool inverted) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + Span rowSpan = row.GetSpan(); + + // We need to set each alpha component value to fully opaque. + for (int x = 1; x < rowSpan.Length; x += 2) + { + rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); + } + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + rowSpan, + pixelSpan, + width); + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 24 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadBgr24(int width, int height, Buffer2D pixels, bool inverted) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// Reads a uncompressed TGA image where each pixels has 32 bit. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + } + } + + /// + /// Reads a run length encoded TGA image. + /// + /// The pixel type. + /// The width of the image. + /// The height of the image. + /// The to assign the palette to. + /// The bytes per pixel. + /// Indicates, if the origin of the image is top left rather the bottom left (the default). + private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel, bool inverted) + where TPixel : struct, IPixel + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel); + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + int rowStartIdx = y * width * bytesPerPixel; + for (int x = 0; x < width; x++) + { + int idx = rowStartIdx + (x * bytesPerPixel); + switch (bytesPerPixel) + { + case 1: + color.FromGray8(Unsafe.As(ref bufferSpan[idx])); + break; + case 2: + // Set alpha value to 1, to treat it as opaque for Bgra5551. + bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); + color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); + break; + case 3: + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + break; + case 4: + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + break; + } + + pixelRow[x] = color; + } + } + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public IImageInfo Identify(Stream stream) + { + this.ReadFileHeader(stream); + return new ImageInfo( + new PixelTypeInfo(this.fileHeader.PixelDepth), + this.fileHeader.Width, + this.fileHeader.Height, + this.metadata); + } + + /// + /// Produce uncompressed tga data from a run length encoded stream. + /// + /// The width of the image. + /// The height of the image. + /// Buffer for uncompressed data. + /// The bytes used per pixel. + private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) + { + int uncompressedPixels = 0; + var pixel = new byte[bytesPerPixel]; + int totalPixels = width * height; + while (uncompressedPixels < totalPixels) + { + byte runLengthByte = (byte)this.currentStream.ReadByte(); + + // The high bit of a run length packet is set to 1. + int highBit = runLengthByte >> 7; + if (highBit == 1) + { + int runLength = runLengthByte & 127; + this.currentStream.Read(pixel, 0, bytesPerPixel); + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += bytesPerPixel; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + int bufferIdx = uncompressedPixels * bytesPerPixel; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, bytesPerPixel); + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += bytesPerPixel; + } + } + } + } + + /// + /// Returns the y- value based on the given height. + /// + /// The y- value representing the current row. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + /// The representing the inverted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; + + /// + /// Reads the tga file header from the stream. + /// + /// The containing image data. + /// true, if the image origin is top left. + private bool ReadFileHeader(Stream stream) + { + this.currentStream = stream; + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + var buffer = new byte[TgaFileHeader.Size]; +#endif + this.currentStream.Read(buffer, 0, TgaFileHeader.Size); + this.fileHeader = TgaFileHeader.Parse(buffer); + this.metadata = new ImageMetadata(); + this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); + this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; + + // Bit at position 5 of the descriptor indicates, that the origin is top left instead of bottom right. + if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) + { + return true; + } + + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs new file mode 100644 index 0000000000..2fcbb822f5 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Image encoder for writing an image to a stream as a targa truevision image. + /// + public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions + { + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel? BitsPerPixel { get; set; } + + /// + /// Gets or sets a value indicating whether no compression or run length compression should be used. + /// + public TgaCompression Compression { get; set; } = TgaCompression.RunLength; + + /// + public void Encode(Image image, Stream stream) + where TPixel : struct, IPixel + { + var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); + encoder.Encode(image, stream); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs new file mode 100644 index 0000000000..28b87e9857 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -0,0 +1,348 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Image encoder for writing an image to a stream as a truevision targa image. + /// + internal sealed class TgaEncoderCore + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// Reusable buffer for writing data. + /// + private readonly byte[] buffer = new byte[2]; + + /// + /// The color depth, in number of bits per pixel. + /// + private TgaBitsPerPixel? bitsPerPixel; + + /// + /// Indicates if run length compression should be used. + /// + private readonly TgaCompression compression; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The memory manager. + public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.bitsPerPixel = options.BitsPerPixel; + this.compression = options.Compression; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void Encode(Image image, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.GetConfiguration(); + ImageMetadata metadata = image.Metadata; + TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); + this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; + + TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) + { + imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + } + + // If compression is used, set bit 5 of the image descriptor to indicate an left top origin. + byte imageDescriptor = (byte)(this.compression is TgaCompression.RunLength ? 32 : 0); + + var fileHeader = new TgaFileHeader( + idLength: 0, + colorMapType: 0, + imageType: imageType, + cMapStart: 0, + cMapLength: 0, + cMapDepth: 0, + xOffset: 0, + yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. + width: (short)image.Width, + height: (short)image.Height, + pixelDepth: (byte)this.bitsPerPixel.Value, + imageDescriptor: imageDescriptor); + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + byte[] buffer = new byte[TgaFileHeader.Size]; +#endif + fileHeader.WriteTo(buffer); + + stream.Write(buffer, 0, TgaFileHeader.Size); + + if (this.compression is TgaCompression.RunLength) + { + this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); + } + else + { + this.WriteImage(stream, image.Frames.RootFrame); + } + + stream.Flush(); + } + + /// + /// Writes the pixel data to the binary stream. + /// + /// The pixel format. + /// The to write to. + /// + /// The containing pixel data. + /// + private void WriteImage(Stream stream, ImageFrame image) + where TPixel : struct, IPixel + { + Buffer2D pixels = image.PixelBuffer; + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + this.Write8Bit(stream, pixels); + break; + + case TgaBitsPerPixel.Pixel16: + this.Write16Bit(stream, pixels); + break; + + case TgaBitsPerPixel.Pixel24: + this.Write24Bit(stream, pixels); + break; + + case TgaBitsPerPixel.Pixel32: + this.Write32Bit(stream, pixels); + break; + } + } + + /// + /// Writes a run length encoded tga image to the stream. + /// + /// The pixel type. + /// The stream to write the image to. + /// The image to encode. + private void WriteRunLengthEndcodedImage(Stream stream, ImageFrame image) + where TPixel : struct, IPixel + { + Rgba32 color = default; + Buffer2D pixels = image.PixelBuffer; + Span pixelSpan = pixels.GetSpan(); + int totalPixels = image.Width * image.Height; + int encodedPixels = 0; + while (encodedPixels < totalPixels) + { + TPixel currentPixel = pixelSpan[encodedPixels]; + currentPixel.ToRgba32(ref color); + byte equalPixelCount = this.FindEqualPixels(pixelSpan.Slice(encodedPixels)); + + // Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. + stream.WriteByte((byte)(equalPixelCount | 128)); + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + int luminance = GetLuminance(currentPixel); + stream.WriteByte((byte)luminance); + break; + + case TgaBitsPerPixel.Pixel16: + var bgra5551 = new Bgra5551(color.ToVector4()); + BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); + stream.WriteByte(this.buffer[0]); + stream.WriteByte(this.buffer[1]); + + break; + + case TgaBitsPerPixel.Pixel24: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + break; + + case TgaBitsPerPixel.Pixel32: + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + stream.WriteByte(color.A); + break; + } + + encodedPixels += equalPixelCount + 1; + } + } + + /// + /// Finds consecutive pixels, which have the same value starting from the pixel span offset 0. + /// + /// The pixel type. + /// The pixel span to search in. + /// The number of equal pixels. + private byte FindEqualPixels(Span pixelSpan) + where TPixel : struct, IPixel + { + int idx = 0; + byte equalPixelCount = 0; + while (equalPixelCount < 127 && idx < pixelSpan.Length - 1) + { + TPixel currentPixel = pixelSpan[idx]; + TPixel nextPixel = pixelSpan[idx + 1]; + if (currentPixel.Equals(nextPixel)) + { + equalPixelCount++; + } + else + { + return equalPixelCount; + } + + idx++; + } + + return equalPixelCount; + } + + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); + + /// + /// Writes the 8bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write8Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 1)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToGray8Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + + /// + /// Writes the 16bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write16Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 2)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgra5551Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + + /// + /// Writes the 24bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write24Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 3)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgr24Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + + /// + /// Writes the 32bit pixels uncompressed to the stream. + /// + /// The pixel format. + /// The to write to. + /// The containing pixel data. + private void Write32Bit(Stream stream, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.AllocateRow(pixels.Width, 4)) + { + for (int y = pixels.Height - 1; y >= 0; y--) + { + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.ToBgra32Bytes( + this.configuration, + pixelSpan, + row.GetSpan(), + pixelSpan.Length); + stream.Write(row.Array, 0, row.Length()); + } + } + } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The pixel to get the luminance from. + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(TPixel sourcePixel) + where TPixel : struct, IPixel + { + var vector = sourcePixel.ToVector4(); + return ImageMaths.GetBT709Luminance(ref vector, 256); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs new file mode 100644 index 0000000000..e2bbb6fbd2 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// This block of bytes tells the application detailed information about the targa image. + /// + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct TgaFileHeader + { + /// + /// Defines the size of the data structure in the targa file. + /// + public const int Size = TgaConstants.FileHeaderLength; + + public TgaFileHeader( + byte idLength, + byte colorMapType, + TgaImageType imageType, + short cMapStart, + short cMapLength, + byte cMapDepth, + short xOffset, + short yOffset, + short width, + short height, + byte pixelDepth, + byte imageDescriptor) + { + this.IdLength = idLength; + this.ColorMapType = colorMapType; + this.ImageType = imageType; + this.CMapStart = cMapStart; + this.CMapLength = cMapLength; + this.CMapDepth = cMapDepth; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.Width = width; + this.Height = height; + this.PixelDepth = pixelDepth; + this.ImageDescriptor = imageDescriptor; + } + + /// + /// Gets the id length. + /// This field identifies the number of bytes contained in Field 6, the Image ID Field. The maximum number + /// of characters is 255. A value of zero indicates that no Image ID field is included with the image. + /// + public byte IdLength { get; } + + /// + /// Gets the color map type. + /// This field indicates the type of color map (if any) included with the image. There are currently 2 defined + /// values for this field: + /// 0 - indicates that no color-map data is included with this image. + /// 1 - indicates that a color-map is included with this image. + /// + public byte ColorMapType { get; } + + /// + /// Gets the image type. + /// The TGA File Format can be used to store Pseudo-Color, True-Color and Direct-Color images of various + /// pixel depths. + /// + public TgaImageType ImageType { get; } + + /// + /// Gets the start of the color map. + /// This field and its sub-fields describe the color map (if any) used for the image. If the Color Map Type field + /// is set to zero, indicating that no color map exists, then these 5 bytes should be set to zero. + /// + public short CMapStart { get; } + + /// + /// Gets the total number of color map entries included. + /// + public short CMapLength { get; } + + /// + /// Gets the number of bits per entry. Typically 15, 16, 24 or 32-bit values are used. + /// + public byte CMapDepth { get; } + + /// + /// Gets the XOffset. + /// These bytes specify the absolute horizontal coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short XOffset { get; } + + /// + /// Gets the YOffset. + /// These bytes specify the absolute vertical coordinate for the lower left + /// corner of the image as it is positioned on a display device having an + /// origin at the lower left of the screen. + /// + public short YOffset { get; } + + /// + /// Gets the width of the image in pixels. + /// + public short Width { get; } + + /// + /// Gets the height of the image in pixels. + /// + public short Height { get; } + + /// + /// Gets the number of bits per pixel. This number includes + /// the Attribute or Alpha channel bits. Common values are 8, 16, 24 and + /// 32 but other pixel depths could be used. + /// + public byte PixelDepth { get; } + + /// + /// Gets the ImageDescriptor. + /// ImageDescriptor contains two pieces of information. + /// Bits 0 through 3 contain the number of attribute bits per pixel. + /// Attribute bits are found only in pixels for the 16- and 32-bit flavors of the TGA format and are called alpha channel, + /// overlay, or interrupt bits. Bits 4 and 5 contain the image origin location (coordinate 0,0) of the image. + /// This position may be any of the four corners of the display screen. + /// When both of these bits are set to zero, the image origin is the lower-left corner of the screen. + /// Bits 6 and 7 of the ImageDescriptor field are unused and should be set to 0. + /// + public byte ImageDescriptor { get; } + + public static TgaFileHeader Parse(Span data) + { + return MemoryMarshal.Cast(data)[0]; + } + + public void WriteTo(Span buffer) + { + ref TgaFileHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + + dest = this; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFormat.cs b/src/ImageSharp/Formats/Tga/TgaFormat.cs new file mode 100644 index 0000000000..badb1d77a4 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFormat.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the tga format. + /// + public sealed class TgaFormat : IImageFormat + { + /// + /// Gets the current instance. + /// + public static TgaFormat Instance { get; } = new TgaFormat(); + + /// + public string Name => "TGA"; + + /// + public string DefaultMimeType => "image/tga"; + + /// + public IEnumerable MimeTypes => TgaConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => TgaConstants.FileExtensions; + + /// + public TgaMetadata CreateDefaultFormatMetadata() => new TgaMetadata(); + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs new file mode 100644 index 0000000000..bd9cfa900c --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Detects tga file headers. + /// + public sealed class TgaImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => TgaConstants.FileHeaderLength; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + return this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + // There is no magick bytes in a tga file, so at least the image type + // and the colormap type in the header will be checked for a valid value. + if (header[1] != 0 && header[1] != 1) + { + return false; + } + + var imageType = (TgaImageType)header[2]; + return imageType.IsValid(); + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs new file mode 100644 index 0000000000..491fd3ea77 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors. + ImageSharp.Formats.Tga +{ + /// + /// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color, + /// True-Color and Direct-Color images of various pixel depths. + /// + public enum TgaImageType : byte + { + /// + /// No image data included. + /// + NoImageData = 0, + + /// + /// Uncompressed, color mapped image. + /// + ColorMapped = 1, + + /// + /// Uncompressed true color image. + /// + TrueColor = 2, + + /// + /// Uncompressed Black and white (grayscale) image. + /// + BlackAndWhite = 3, + + /// + /// Run length encoded, color mapped image. + /// + RleColorMapped = 9, + + /// + /// Run length encoded, true color image. + /// + RleTrueColor = 10, + + /// + /// Run length encoded, black and white (grayscale) image. + /// + RleBlackAndWhite = 11, + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs new file mode 100644 index 0000000000..6a30cdddd7 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Extension methods for TgaImageType enum. + /// + public static class TgaImageTypeExtensions + { + /// + /// Checks if this tga image type is run length encoded. + /// + /// The tga image type. + /// True, if this image type is run length encoded, otherwise false. + public static bool IsRunLengthEncoded(this TgaImageType imageType) + { + if (imageType is TgaImageType.RleColorMapped || imageType is TgaImageType.RleBlackAndWhite || imageType is TgaImageType.RleTrueColor) + { + return true; + } + + return false; + } + + /// + /// Checks, if the image type has valid value. + /// + /// The image type. + /// true, if its a valid tga image type. + public static bool IsValid(this TgaImageType imageType) + { + switch (imageType) + { + case TgaImageType.NoImageData: + case TgaImageType.ColorMapped: + case TgaImageType.TrueColor: + case TgaImageType.BlackAndWhite: + case TgaImageType.RleColorMapped: + case TgaImageType.RleTrueColor: + case TgaImageType.RleBlackAndWhite: + return true; + + default: + return false; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs new file mode 100644 index 0000000000..4ce61d2e48 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tga +{ + /// + /// Provides TGA specific metadata information for the image. + /// + public class TgaMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public TgaMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private TgaMetadata(TgaMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + } + + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; + + /// + public IDeepCloneable DeepClone() => new TgaMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs new file mode 100644 index 0000000000..845d009227 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Tga +{ + internal static class TgaThrowHelper + { + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowImageFormatException(string errorMessage) + { + throw new ImageFormatException(errorMessage); + } + + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowNotSupportedException(string errorMessage) + { + throw new NotSupportedException(errorMessage); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt index 8603012321..459924c318 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt @@ -18,7 +18,7 @@ /// /// Converts all pixels in 'source` span of into a span of -s. /// - /// A to configure internal operations + /// A to configure internal operations. /// The source of data. /// The to the destination pixels. internal virtual void From<#=pixelType#>(Configuration configuration, ReadOnlySpan<<#=pixelType#>> source, Span destPixels) @@ -41,7 +41,7 @@ /// A helper for that expects a byte span. /// The layout of the data in 'sourceBytes' must be compatible with layout. /// - /// A to configure internal operations + /// A to configure internal operations. /// The to the source bytes. /// The to the destination pixels. /// The number of pixels to convert. diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index f2f11cbfe5..622c133aeb 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -349,7 +349,7 @@ private void AddPixelsToHistogram(ref Vector4 greyValuesBase, ref int histogramB { for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ImageMaths.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, luminance)++; } } @@ -366,7 +366,7 @@ private void RemovePixelsFromHistogram(ref Vector4 greyValuesBase, ref int histo { for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ImageMaths.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, luminance)--; } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs index 6e4c16de76..284b9de1f6 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs @@ -143,16 +143,7 @@ public void ClipHistogram(Span histogram, int clipLimit) public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) { var vector = sourcePixel.ToVector4(); - return GetLuminance(ref vector, luminanceLevels); + return ImageMaths.GetBT709Luminance(ref vector, luminanceLevels); } - - /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. - /// - /// The vector to get the luminance from - /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) - [MethodImpl(InliningOptions.ShortMethod)] - public static int GetLuminance(ref Vector4 vector, int luminanceLevels) - => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs new file mode 100644 index 0000000000..e3c7216102 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using BenchmarkDotNet.Attributes; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [Config(typeof(Config.ShortClr))] + public class DecodeTga : BenchmarkBase + { + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Tga.Bit24)] + public string TestImage { get; set; } + + [Benchmark(Baseline = true, Description = "ImageMagick Tga")] + public Size TgaImageMagick() + { + using (var magickImage = new MagickImage(this.TestImageFullPath)) + { + return new Size(magickImage.Width, magickImage.Height); + } + } + + [Benchmark(Description = "ImageSharp Tga")] + public Size TgaCore() + { + using (var image = Image.Load(this.TestImageFullPath)) + { + return new Size(image.Width, image.Height); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs new file mode 100644 index 0000000000..ddcbec218e --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using BenchmarkDotNet.Attributes; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [Config(typeof(Config.ShortClr))] + public class EncodeTga : BenchmarkBase + { + private MagickImage tgaMagick; + private Image tgaCore; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Tga.Bit24)] + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.tgaCore == null) + { + this.tgaCore = Image.Load(TestImageFullPath); + this.tgaMagick = new MagickImage(this.TestImageFullPath); + } + } + + [Benchmark(Baseline = true, Description = "Magick Tga")] + public void BmpSystemDrawing() + { + using (var memoryStream = new MemoryStream()) + { + this.tgaMagick.Write(memoryStream, MagickFormat.Tga); + } + } + + [Benchmark(Description = "ImageSharp Tga")] + public void BmpCore() + { + using (var memoryStream = new MemoryStream()) + { + this.tgaCore.SaveAsBmp(memoryStream); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 14ad5635cd..a57d388a95 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index a0e552aebf..6b35bbb972 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -20,6 +20,8 @@ public class ConfigurationTests public Configuration ConfigurationEmpty { get; } public Configuration DefaultConfiguration { get; } + private readonly int expectedDefaultConfigurationCount = 5; + public ConfigurationTests() { // the shallow copy of configuration should behave exactly like the default configuration, @@ -108,14 +110,13 @@ public void AddFormatCallsConfig() [Fact] public void ConfigurationCannotAddDuplicates() { - const int count = 4; Configuration config = this.DefaultConfiguration; - Assert.Equal(count, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); config.ImageFormatsManager.AddImageFormat(BmpFormat.Instance); - Assert.Equal(count, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] @@ -123,7 +124,7 @@ public void DefaultConfigurationHasCorrectFormatCount() { Configuration config = Configuration.CreateDefaultInstance(); - Assert.Equal(4, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 25cf29406e..4c3fe31493 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -1,4 +1,11 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System; +using System.IO; +using System.Linq; + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using Xunit; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2a76310fcd..e064c0fb06 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -218,7 +218,7 @@ public void Issue1014(TestImageProvider provider) using (Image image = provider.GetImage(new PngDecoder())) { image.DebugSave(provider); - // TODO: compare to expected output + image.CompareToOriginal(provider, ImageComparer.Exact); } }); Assert.Null(ex); diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs new file mode 100644 index 0000000000..03ad10de40 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming + +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + using static TestImages.Tga; + + public class TgaDecoderTests + { + [Theory] + [WithFile(Grey, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit15, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_15Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit15Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_15Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16PalRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_WithPalette_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24RleTopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Palette_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(GreyRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLengthEncoded_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16Pal, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WithPalette_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Pal, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WithPalette_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs new file mode 100644 index 0000000000..e946729a15 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming + +using System.IO; + +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + using static TestImages.Tga; + + public class TgaEncoderTests + { + public static readonly TheoryData BitsPerPixel = + new TheoryData + { + TgaBitsPerPixel.Pixel24, + TgaBitsPerPixel.Pixel32 + }; + + public static readonly TheoryData TgaBitsPerPixelFiles = + new TheoryData + { + { Grey, TgaBitsPerPixel.Pixel8 }, + { Bit32, TgaBitsPerPixel.Pixel32 }, + { Bit24, TgaBitsPerPixel.Pixel24 }, + { Bit16, TgaBitsPerPixel.Pixel16 }, + }; + + [Theory] + [MemberData(nameof(TgaBitsPerPixelFiles))] + public void Encode_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) + { + var options = new TgaEncoder(); + + TestFile testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateRgba32Image()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + memStream.Position = 0; + using (Image output = Image.Load(memStream)) + { + TgaMetadata meta = output.Metadata.GetFormatMetadata(TgaFormat.Instance); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); + } + } + } + } + + [Theory] + [MemberData(nameof(TgaBitsPerPixelFiles))] + public void Encode_WithCompression_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) + { + var options = new TgaEncoder() + { + Compression = TgaCompression.RunLength + }; + + TestFile testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateRgba32Image()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + memStream.Position = 0; + using (Image output = Image.Load(memStream)) + { + TgaMetadata meta = output.Metadata.GetFormatMetadata(TgaFormat.Instance); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); + } + } + } + } + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false, compareTolerance: 0.03f); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + // using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false, compareTolerance: 0.03f); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); + + private static void TestTgaEncoderCore( + TestImageProvider provider, + TgaBitsPerPixel bitsPerPixel, + TgaCompression compression = TgaCompression.None, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression}; + + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + memStream.Position = 0; + using (var encodedImage = (Image)Image.Load(memStream)) + { + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs new file mode 100644 index 0000000000..c227b79576 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + public class TgaFileHeaderTests + { + private static readonly byte[] Data = { + 0, + 0, + 15 // invalid tga image type + }; + + private MemoryStream Stream { get; } = new MemoryStream(Data); + + [Fact] + public void ImageLoad_WithInvalidImageType_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs new file mode 100644 index 0000000000..a2f2e86d7d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; + +using ImageMagick; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + public static class TgaTestUtils + { + public static void CompareWithReferenceDecoder(TestImageProvider provider, + Image image, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : struct, IPixel + { + string path = TestImageProvider.GetFilePathOrNull(provider); + if (path == null) + { + throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); + } + + TestFile testFile = TestFile.Create(path); + Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + if (useExactComparer) + { + ImageComparer.Exact.VerifySimilarity(magickImage, image); + } + else + { + ImageComparer.Tolerant(compareTolerance).VerifySimilarity(magickImage, image); + } + } + + public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) + where TPixel : struct, IPixel + { + using (var magickImage = new MagickImage(fileInfo)) + { + var result = new Image(configuration, magickImage.Width, magickImage.Height); + Span resultPixels = result.GetPixelSpan(); + + using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels, + resultPixels.Length); + } + + return result; + } + } + } +} diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs index 018fabd982..817672f34a 100644 --- a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs @@ -2,6 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; + using Xunit; namespace SixLabors.ImageSharp.Tests.Helpers @@ -131,6 +136,23 @@ public void LeastCommonMultiple(int a, int b, int expected) Assert.Equal(expected, actual); } + [Theory] + [InlineData(0.2f, 0.7f, 0.1f, 256, 140)] + [InlineData(0.5f, 0.5f, 0.5f, 256, 128)] + [InlineData(0.5f, 0.5f, 0.5f, 65536, 32768)] + [InlineData(0.2f, 0.7f, 0.1f, 65536, 36069)] + public void GetBT709Luminance_WithVector4(float x, float y, float z, int luminanceLevels, int expected) + { + // arrange + var vector = new Vector4(x, y, z, 0.0f); + + // act + int actual = ImageMaths.GetBT709Luminance(ref vector, luminanceLevels); + + // assert + Assert.Equal(expected, actual); + } + // TODO: We need to test all ImageMaths methods! } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 146f2efcdb..1777498694 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -365,5 +365,24 @@ public static class Issues public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; } + + public static class Tga + { + public const string Bit15 = "Tga/rgb15.tga"; + public const string Bit15Rle = "Tga/rgb15rle.tga"; + public const string Bit16 = "Tga/targa_16bit.tga"; + public const string Bit16PalRle = "Tga/ccm8.tga"; + public const string Bit24 = "Tga/targa_24bit.tga"; + public const string Bit24TopLeft = "Tga/targa_24bit_pal_origin_topleft.tga"; + public const string Bit24RleTopLeft = "Tga/targa_24bit_rle_origin_topleft.tga"; + public const string Bit32 = "Tga/targa_32bit.tga"; + public const string Grey = "Tga/targa_8bit.tga"; + public const string GreyRle = "Tga/targa_8bit_rle.tga"; + public const string Bit16Rle = "Tga/targa_16bit_rle.tga"; + public const string Bit24Rle = "Tga/targa_24bit_rle.tga"; + public const string Bit32Rle = "Tga/targa_32bit_rle.tga"; + public const string Bit16Pal = "Tga/targa_16bit_pal.tga"; + public const string Bit24Pal = "Tga/targa_24bit_pal.tga"; + } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 7d06847223..e09b27c714 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests @@ -53,7 +54,8 @@ private static Configuration CreateDefaultConfiguration() { var cfg = new Configuration( new JpegConfigurationModule(), - new GifConfigurationModule() + new GifConfigurationModule(), + new TgaConfigurationModule() ); // Magick codecs should work on all platforms @@ -75,4 +77,4 @@ private static Configuration CreateDefaultConfiguration() return cfg; } } -} \ No newline at end of file +} diff --git a/tests/Images/External b/tests/Images/External index f0c4033667..ca4cf8318f 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit f0c4033667bd23ad9dde82ccb625c232d402ee05 +Subproject commit ca4cf8318fe4d09f0fc825686dcd477ebfb5e3e5 diff --git a/tests/Images/Input/Tga/ccm8.tga b/tests/Images/Input/Tga/ccm8.tga new file mode 100644 index 0000000000..7a21efef7b Binary files /dev/null and b/tests/Images/Input/Tga/ccm8.tga differ diff --git a/tests/Images/Input/Tga/rgb15.tga b/tests/Images/Input/Tga/rgb15.tga new file mode 100644 index 0000000000..9506b418ae Binary files /dev/null and b/tests/Images/Input/Tga/rgb15.tga differ diff --git a/tests/Images/Input/Tga/rgb15rle.tga b/tests/Images/Input/Tga/rgb15rle.tga new file mode 100644 index 0000000000..5d4672c9cf Binary files /dev/null and b/tests/Images/Input/Tga/rgb15rle.tga differ diff --git a/tests/Images/Input/Tga/targa.png b/tests/Images/Input/Tga/targa.png new file mode 100644 index 0000000000..c18cf7e23d Binary files /dev/null and b/tests/Images/Input/Tga/targa.png differ diff --git a/tests/Images/Input/Tga/targa_16bit.tga b/tests/Images/Input/Tga/targa_16bit.tga new file mode 100644 index 0000000000..eeaf33d592 Binary files /dev/null and b/tests/Images/Input/Tga/targa_16bit.tga differ diff --git a/tests/Images/Input/Tga/targa_16bit_pal.tga b/tests/Images/Input/Tga/targa_16bit_pal.tga new file mode 100644 index 0000000000..8074ad78f5 Binary files /dev/null and b/tests/Images/Input/Tga/targa_16bit_pal.tga differ diff --git a/tests/Images/Input/Tga/targa_16bit_rle.tga b/tests/Images/Input/Tga/targa_16bit_rle.tga new file mode 100644 index 0000000000..968f4de320 Binary files /dev/null and b/tests/Images/Input/Tga/targa_16bit_rle.tga differ diff --git a/tests/Images/Input/Tga/targa_24bit.tga b/tests/Images/Input/Tga/targa_24bit.tga new file mode 100644 index 0000000000..ebe78aea5f Binary files /dev/null and b/tests/Images/Input/Tga/targa_24bit.tga differ diff --git a/tests/Images/Input/Tga/targa_24bit_pal.tga b/tests/Images/Input/Tga/targa_24bit_pal.tga new file mode 100644 index 0000000000..d97c646748 Binary files /dev/null and b/tests/Images/Input/Tga/targa_24bit_pal.tga differ diff --git a/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga new file mode 100644 index 0000000000..0c3ad641d1 Binary files /dev/null and b/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga differ diff --git a/tests/Images/Input/Tga/targa_24bit_rle.tga b/tests/Images/Input/Tga/targa_24bit_rle.tga new file mode 100644 index 0000000000..e887af750b Binary files /dev/null and b/tests/Images/Input/Tga/targa_24bit_rle.tga differ diff --git a/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga new file mode 100644 index 0000000000..53e08c5e2c Binary files /dev/null and b/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga differ diff --git a/tests/Images/Input/Tga/targa_32bit.tga b/tests/Images/Input/Tga/targa_32bit.tga new file mode 100644 index 0000000000..7f29e022fd Binary files /dev/null and b/tests/Images/Input/Tga/targa_32bit.tga differ diff --git a/tests/Images/Input/Tga/targa_32bit_rle.tga b/tests/Images/Input/Tga/targa_32bit_rle.tga new file mode 100644 index 0000000000..c5af174b1f Binary files /dev/null and b/tests/Images/Input/Tga/targa_32bit_rle.tga differ diff --git a/tests/Images/Input/Tga/targa_8bit.tga b/tests/Images/Input/Tga/targa_8bit.tga new file mode 100644 index 0000000000..0b69052ec2 Binary files /dev/null and b/tests/Images/Input/Tga/targa_8bit.tga differ diff --git a/tests/Images/Input/Tga/targa_8bit_rle.tga b/tests/Images/Input/Tga/targa_8bit_rle.tga new file mode 100644 index 0000000000..21e9dfa3dd Binary files /dev/null and b/tests/Images/Input/Tga/targa_8bit_rle.tga differ