From 16aef1757b379fdf840e450a5397b80cc26a30ab Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 12:23:39 +0200 Subject: [PATCH 01/40] Add decoding of 24 bit targa files --- src/ImageSharp/Configuration.cs | 7 +- .../Formats/Tga/ITgaDecoderOptions.cs | 12 ++ .../Formats/Tga/TgaConfigurationModule.cs | 18 +++ src/ImageSharp/Formats/Tga/TgaConstants.cs | 20 +++ src/ImageSharp/Formats/Tga/TgaDecoder.cs | 34 +++++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 125 ++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 139 ++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFormat.cs | 33 +++++ .../Formats/Tga/TgaImageFormatDetector.cs | 27 ++++ src/ImageSharp/Formats/Tga/TgaImageType.cs | 48 ++++++ src/ImageSharp/Formats/Tga/TgaMetadata.cs | 21 +++ 11 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/ITgaDecoderOptions.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaConstants.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaDecoder.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaFileHeader.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaFormat.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaImageType.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaMetadata.cs diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 0d44db8d87..e385fba5e0 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Bmp; 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 @@ namespace SixLabors.ImageSharp /// /// /// . + /// . /// /// The default configuration of . internal static Configuration CreateDefaultInstance() @@ -158,7 +160,8 @@ namespace SixLabors.ImageSharp new PngConfigurationModule(), new JpegConfigurationModule(), new GifConfigurationModule(), - new BmpConfigurationModule()); + new BmpConfigurationModule(), + new TgaConfigurationModule()); } } -} \ No newline at end of file +} 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/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs new file mode 100644 index 0000000000..c7bf6cc93d --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -0,0 +1,18 @@ +// 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.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..88c98b06a9 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -0,0 +1,20 @@ +// 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" }; + } +} 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..b8068c0825 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +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 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; + + 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 + { + this.ReadFileHeader(stream); + + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + + return image; + } + catch (Exception e) + { + throw new ImageFormatException("TGA image does not have a valid format.", e); + } + } + + /// + /// 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); + } + + /// + /// Reads the tga file header from the stream. + /// + /// The containing image data. + private void 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); + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs new file mode 100644 index 0000000000..51390e1b73 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +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 = 18; + + 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]; + } + } +} 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..e305728473 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -0,0 +1,27 @@ +// 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 => 18; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + return this.IsSupportedFileFormat(header) ? TgaFormat.Instance : null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize; + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs new file mode 100644 index 0000000000..d8140d5c6e --- /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. + /// Not sure what this is used for. + /// + 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/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs new file mode 100644 index 0000000000..185eaedc9a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -0,0 +1,21 @@ +// 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() + { + } + + /// + public IDeepCloneable DeepClone() => throw new System.NotImplementedException(); + } +} From 38589a0eb825c89c8b589294cce17b571341f2a1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 16:32:32 +0200 Subject: [PATCH 02/40] Add support for decoding 8, 16, 32 bit tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 105 ++++++++++++++++--- src/ImageSharp/Formats/Tga/TgaThrowHelper.cs | 21 ++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TgaThrowHelper.cs diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index b8068c0825..a73f04bcdb 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -69,28 +69,109 @@ namespace SixLabors.ImageSharp.Formats.Tga var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + switch (this.fileHeader.PixelDepth) { - for (int y = 0; y < this.fileHeader.Height; y++) - { - this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); - PixelOperations.Instance.FromBgr24Bytes( - this.configuration, - row.GetSpan(), - pixelSpan, - this.fileHeader.Width); - } + case 8: + this.ReadMonoChrome(pixels); + break; + + case 16: + this.ReadBgra16(pixels); + break; + + case 24: + this.ReadBgr24(pixels); + break; + + case 32: + this.ReadBgra32(pixels); + break; + + default: + TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); + break; } return image; } - catch (Exception e) + catch (IndexOutOfRangeException e) { throw new ImageFormatException("TGA image does not have a valid format.", e); } } + private void ReadMonoChrome(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 1, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromGray8Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgra16(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgr24(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + + private void ReadBgra32(Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 4, 0)) + { + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + this.fileHeader.Width); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs new file mode 100644 index 0000000000..9e36b20539 --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -0,0 +1,21 @@ +// 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 ThrowNotSupportedException(string errorMessage) + { + throw new NotSupportedException(errorMessage); + } + } +} From 3b48dc39dcff1d424e94b34bbc842c5fb779bec8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 18:08:30 +0200 Subject: [PATCH 03/40] Add tga decoder tests --- .gitattributes | 1 + src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 12 +- .../PixelOperations{TPixel}.Generated.tt | 4 +- .../Formats/Tga/TgaDecoderTests.cs | 107 ++++++++++++++++++ .../ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- tests/ImageSharp.Tests/TestImages.cs | 8 ++ .../TestUtilities/TestEnvironment.Formats.cs | 6 +- tests/Images/Input/Tga/bike_16bit.tga | 3 + tests/Images/Input/Tga/bike_24bit.tga | 3 + tests/Images/Input/Tga/bike_32bit.tga | 3 + tests/Images/Input/Tga/bike_8bit.tga | 3 + 11 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs create mode 100644 tests/Images/Input/Tga/bike_16bit.tga create mode 100644 tests/Images/Input/Tga/bike_24bit.tga create mode 100644 tests/Images/Input/Tga/bike_32bit.tga create mode 100644 tests/Images/Input/Tga/bike_8bit.tga diff --git a/.gitattributes b/.gitattributes index b9a9ddd4c3..195506770b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -68,6 +68,7 @@ *.gif binary *.jpg binary *.png binary +*.tga binary *.ttf binary *.snk binary # diff as plain text diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index a73f04bcdb..abad2c5e77 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -108,7 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), @@ -126,7 +127,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), @@ -144,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), @@ -162,7 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), @@ -201,6 +202,9 @@ namespace SixLabors.ImageSharp.Formats.Tga #endif this.currentStream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); + + // TODO: no meta data yet. + this.metadata = new ImageMetadata(); } } } 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/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs new file mode 100644 index 0000000000..d998ac292b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using ImageMagick; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +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); + 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); + 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); + 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); + CompareWithReferenceDecoder(provider, image); + } + } + + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) + 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 = this.DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + ImageComparer.Exact.VerifySimilarity(image, magickImage); + } + + private 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/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 1ac5f8085a..1f6b8b4d95 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 146f2efcdb..bd1a0d3913 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -365,5 +365,13 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; } + + public static class Tga + { + public const string Bit32 = "Tga/bike_32bit.tga"; + public const string Bit24 = "Tga/bike_24bit.tga"; + public const string Bit16 = "Tga/bike_16bit.tga"; + public const string Grey = "Tga/bike_8bit.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.Bmp; 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 @@ namespace SixLabors.ImageSharp.Tests { var cfg = new Configuration( new JpegConfigurationModule(), - new GifConfigurationModule() + new GifConfigurationModule(), + new TgaConfigurationModule() ); // Magick codecs should work on all platforms @@ -75,4 +77,4 @@ namespace SixLabors.ImageSharp.Tests return cfg; } } -} \ No newline at end of file +} diff --git a/tests/Images/Input/Tga/bike_16bit.tga b/tests/Images/Input/Tga/bike_16bit.tga new file mode 100644 index 0000000000..00489d94a0 --- /dev/null +++ b/tests/Images/Input/Tga/bike_16bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b2bc922e2397ce8cd8b1e7792f20e2c7edad68ad8fac037a5b91bba5148a80b +size 97518 diff --git a/tests/Images/Input/Tga/bike_24bit.tga b/tests/Images/Input/Tga/bike_24bit.tga new file mode 100644 index 0000000000..a5e46f794d --- /dev/null +++ b/tests/Images/Input/Tga/bike_24bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26d55794c9b012b07517d1129630ccc35ca56015cbdd481debea00826130f925 +size 146268 diff --git a/tests/Images/Input/Tga/bike_32bit.tga b/tests/Images/Input/Tga/bike_32bit.tga new file mode 100644 index 0000000000..70e755d1f1 --- /dev/null +++ b/tests/Images/Input/Tga/bike_32bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb3774b695d2409f3e7584dc67ce7b3d8a75377c194e7f33e4cc315b3ae36a35 +size 195018 diff --git a/tests/Images/Input/Tga/bike_8bit.tga b/tests/Images/Input/Tga/bike_8bit.tga new file mode 100644 index 0000000000..ba11619a5e --- /dev/null +++ b/tests/Images/Input/Tga/bike_8bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f3ce893f38d90767c692eb9628026c0421330934a5cbb686813ec26a9606d2 +size 48768 From aa110740e057bab1c1b17d2c4697e681c1165fc1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 20:55:03 +0200 Subject: [PATCH 04/40] Fix decoding 16 bit tga files by making them opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index abad2c5e77..92811fa46d 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; @@ -123,7 +124,10 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) + using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(this.fileHeader.Width)) { + Span bgraRowSpan = bgraRow.GetSpan(); + long currentPosition = this.currentStream.Position; for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); @@ -134,6 +138,28 @@ namespace SixLabors.ImageSharp.Formats.Tga pixelSpan, this.fileHeader.Width); } + + // We need to set each alpha component value to fully opaque. + // Reset our stream for a second pass. + this.currentStream.Position = currentPosition; + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + this.fileHeader.Width); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + + for (int x = 0; x < this.fileHeader.Width; x++) + { + Bgra5551 bgra = bgraRowSpan[x]; + bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); + ref TPixel pixel = ref pixelSpan[x]; + pixel.FromBgra5551(bgra); + } + } } } From 7a8a132558f0398a04d0f912e80f2d82eed67c25 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Oct 2019 21:03:29 +0200 Subject: [PATCH 05/40] Add RLE test images --- .../Formats/Tga/TgaDecoderTests.cs | 48 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 4 ++ tests/Images/Input/Tga/bike_16bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_24bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_32bit_rle.tga | 3 ++ tests/Images/Input/Tga/bike_8bit_rle.tga | 3 ++ 6 files changed, 64 insertions(+) create mode 100644 tests/Images/Input/Tga/bike_16bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_24bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_32bit_rle.tga create mode 100644 tests/Images/Input/Tga/bike_8bit_rle.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index d998ac292b..47983a3ade 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -67,6 +67,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(GreyRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_MonoChrome(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit16Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_16Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit32Rle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_RunLenghtEncoded_32Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bd1a0d3913..51ca48c375 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -372,6 +372,10 @@ namespace SixLabors.ImageSharp.Tests public const string Bit24 = "Tga/bike_24bit.tga"; public const string Bit16 = "Tga/bike_16bit.tga"; public const string Grey = "Tga/bike_8bit.tga"; + public const string Bit32Rle = "Tga/bike_32bit_rle.tga"; + public const string Bit24Rle = "Tga/bike_24bit_rle.tga"; + public const string Bit16Rle = "Tga/bike_16bit_rle.tga"; + public const string GreyRle = "Tga/bike_8bit_rle.tga"; } } } diff --git a/tests/Images/Input/Tga/bike_16bit_rle.tga b/tests/Images/Input/Tga/bike_16bit_rle.tga new file mode 100644 index 0000000000..47c37c50b6 --- /dev/null +++ b/tests/Images/Input/Tga/bike_16bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9f7381f04b3ce23e3144f6b5789d7eccc0c0f010fc901f42d01171556181c1c +size 56938 diff --git a/tests/Images/Input/Tga/bike_24bit_rle.tga b/tests/Images/Input/Tga/bike_24bit_rle.tga new file mode 100644 index 0000000000..65dfb9a1ab --- /dev/null +++ b/tests/Images/Input/Tga/bike_24bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88f7936db92cf536c9656a8ff00c25842c71b93823e05322a1efc3aef2c0a80e +size 106721 diff --git a/tests/Images/Input/Tga/bike_32bit_rle.tga b/tests/Images/Input/Tga/bike_32bit_rle.tga new file mode 100644 index 0000000000..967e9ce7ee --- /dev/null +++ b/tests/Images/Input/Tga/bike_32bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:815e9f32f29a8c51bbd0f2c7d507d6331a709f157fb7bd65188fd07677764c59 +size 141452 diff --git a/tests/Images/Input/Tga/bike_8bit_rle.tga b/tests/Images/Input/Tga/bike_8bit_rle.tga new file mode 100644 index 0000000000..0c6ee06a44 --- /dev/null +++ b/tests/Images/Input/Tga/bike_8bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1531a0f8a95fb4a57e0bde1842b6ee65f5cabfc62ad883d1f9dbc2c5616e498f +size 37259 From 3cbc741a18be2981b1f852cf97d80df1cdafb0a7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:13:01 +0200 Subject: [PATCH 06/40] Add decoding of 24bit RLE tga images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 84 +++++++++++++++++++- src/ImageSharp/Formats/Tga/TgaImageType.cs | 24 +++++- src/ImageSharp/Formats/Tga/TgaThrowHelper.cs | 10 +++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 92811fa46d..692876cea3 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -82,7 +82,15 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case 24: - this.ReadBgr24(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle24(pixels, this.fileHeader.Width, this.fileHeader.Height); + } + else + { + this.ReadBgr24(pixels); + } + break; case 32: @@ -181,6 +189,80 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadRle24(Buffer2D pixels, int width, int height) + where TPixel : struct, IPixel + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle24(width, bufferSpan); + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + for (int x = 0; x < width; x++) + { + int idx = (y * width * 3) + (x * 3); + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + pixelRow[x] = color; + } + } + } + } + + private void UncompressRle24(int w, Span buffer) + { + int uncompressedPixels = 0; +#if NETCOREAPP2_1 + Span pixel = stackalloc byte[3]; +#else + var pixel = new byte[3]; +#endif + int totalPixels = this.fileHeader.Height * this.fileHeader.Width; + while (uncompressedPixels < totalPixels) + { + byte runLengthByte = (byte)this.currentStream.ReadByte(); + + // The high bit of the run length value is always 1, to indicate that this is a run-length encoded packet. + int highBit = runLengthByte >> 7; + if (highBit == 1) + { + int runLength = runLengthByte & 127; + if (runLength == 0) + { + // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); + } + + this.currentStream.Read(pixel, 0, 3); + int bufferIdx = uncompressedPixels * 3; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + buffer[bufferIdx++] = pixel[0]; + buffer[bufferIdx++] = pixel[1]; + buffer[bufferIdx++] = pixel[2]; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + if (runLength == 0) + { + // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); + } + + int bufferIdx = uncompressedPixels * 3; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, 3); + buffer[bufferIdx++] = pixel[0]; + buffer[bufferIdx++] = pixel[1]; + buffer[bufferIdx++] = pixel[2]; + } + } + } + } + private void ReadBgra32(Buffer2D pixels) where TPixel : struct, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index d8140d5c6e..2c19a06954 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.Tga +namespace SixLabors. + ImageSharp.Formats.Tga { /// /// Defines the tga image type. The TGA File Format can be used to store Pseudo-Color, @@ -45,4 +46,25 @@ namespace SixLabors.ImageSharp.Formats.Tga /// RleBlackAndWhite = 11, } + + /// + /// 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; + } + } } diff --git a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs index 9e36b20539..845d009227 100644 --- a/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs +++ b/src/ImageSharp/Formats/Tga/TgaThrowHelper.cs @@ -8,6 +8,16 @@ 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 /// From fec6cad3ea77a74a5cba0e7c741425d226fce0e7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:32:17 +0200 Subject: [PATCH 07/40] Add decoding of 32 bit RLE --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 100 ++++++++++++++----- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 692876cea3..0627be98ca 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -94,7 +94,15 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case 32: - this.ReadBgra32(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle32(pixels, this.fileHeader.Width, this.fileHeader.Height); + } + else + { + this.ReadBgra32(pixels); + } + break; default: @@ -196,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tga using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); - this.UncompressRle24(width, bufferSpan); + this.UncompressRle24(width, height, bufferSpan); for (int y = 0; y < height; y++) { Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); @@ -210,54 +218,38 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void UncompressRle24(int w, Span buffer) + private void UncompressRle24(int width, int height, Span buffer) { int uncompressedPixels = 0; -#if NETCOREAPP2_1 - Span pixel = stackalloc byte[3]; -#else var pixel = new byte[3]; -#endif - int totalPixels = this.fileHeader.Height * this.fileHeader.Width; + int totalPixels = width * height; while (uncompressedPixels < totalPixels) { byte runLengthByte = (byte)this.currentStream.ReadByte(); - // The high bit of the run length value is always 1, to indicate that this is a run-length encoded packet. + // The high bit of a run length packet is set to 1. int highBit = runLengthByte >> 7; if (highBit == 1) { int runLength = runLengthByte & 127; - if (runLength == 0) - { - // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); - } - this.currentStream.Read(pixel, 0, 3); int bufferIdx = uncompressedPixels * 3; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { - buffer[bufferIdx++] = pixel[0]; - buffer[bufferIdx++] = pixel[1]; - buffer[bufferIdx++] = pixel[2]; + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 3; } } else { // Non-run-length encoded packet. int runLength = runLengthByte; - if (runLength == 0) - { - // TgaThrowHelper.ThrowImageFormatException("invalid run length of zero"); - } - int bufferIdx = uncompressedPixels * 3; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { this.currentStream.Read(pixel, 0, 3); - buffer[bufferIdx++] = pixel[0]; - buffer[bufferIdx++] = pixel[1]; - buffer[bufferIdx++] = pixel[2]; + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 3; } } } @@ -281,6 +273,64 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadRle32(Buffer2D pixels, int width, int height) + where TPixel : struct, IPixel + { + TPixel color = default; + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 4, AllocationOptions.Clean)) + { + Span bufferSpan = buffer.GetSpan(); + this.UncompressRle32(width, height, bufferSpan); + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + for (int x = 0; x < width; x++) + { + int idx = (y * width * 4) + (x * 4); + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + pixelRow[x] = color; + } + } + } + } + + private void UncompressRle32(int width, int height, Span buffer) + { + int uncompressedPixels = 0; + var pixel = new byte[4]; + 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, 4); + int bufferIdx = uncompressedPixels * 4; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 4; + } + } + else + { + // Non-run-length encoded packet. + int runLength = runLengthByte; + int bufferIdx = uncompressedPixels * 4; + for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) + { + this.currentStream.Read(pixel, 0, 4); + pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); + bufferIdx += 4; + } + } + } + } + /// /// Reads the raw image information from the specified stream. /// From 81ed8ca8e2fad8df8090ccea819fb3e80d146418 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:41:12 +0200 Subject: [PATCH 08/40] Avoid calculating the row start index inside the loop --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 @@ namespace SixLabors.ImageSharp.Formats.Bmp 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 @@ namespace SixLabors.ImageSharp.Formats.Bmp 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; } From 2bfe960d6a9f8a5a0be8071fb7094856469ffdce Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 19:50:13 +0200 Subject: [PATCH 09/40] Unified reading rle images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 98 +++++--------------- 1 file changed, 25 insertions(+), 73 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 0627be98ca..224a14131c 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Tga case 24: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle24(pixels, this.fileHeader.Width, this.fileHeader.Height); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3); } else { @@ -96,7 +96,7 @@ namespace SixLabors.ImageSharp.Formats.Tga case 32: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle32(pixels, this.fileHeader.Width, this.fileHeader.Height); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4); } else { @@ -197,64 +197,6 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle24(Buffer2D pixels, int width, int height) - where TPixel : struct, IPixel - { - TPixel color = default; - using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) - { - Span bufferSpan = buffer.GetSpan(); - this.UncompressRle24(width, height, bufferSpan); - for (int y = 0; y < height; y++) - { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); - for (int x = 0; x < width; x++) - { - int idx = (y * width * 3) + (x * 3); - color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); - pixelRow[x] = color; - } - } - } - } - - private void UncompressRle24(int width, int height, Span buffer) - { - int uncompressedPixels = 0; - var pixel = new byte[3]; - 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, 3); - int bufferIdx = uncompressedPixels * 3; - for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) - { - pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 3; - } - } - else - { - // Non-run-length encoded packet. - int runLength = runLengthByte; - int bufferIdx = uncompressedPixels * 3; - for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) - { - this.currentStream.Read(pixel, 0, 3); - pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 3; - } - } - } - } - private void ReadBgra32(Buffer2D pixels) where TPixel : struct, IPixel { @@ -273,31 +215,41 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle32(Buffer2D pixels, int width, int height) + private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel) where TPixel : struct, IPixel { TPixel color = default; - using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 4, AllocationOptions.Clean)) + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); - this.UncompressRle32(width, height, bufferSpan); + this.UncompressRle(width, height, bufferSpan, bytesPerPixel); for (int y = 0; y < height; y++) { Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { - int idx = (y * width * 4) + (x * 4); - color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + int idx = rowStartIdx + (x * bytesPerPixel); + switch (bytesPerPixel) + { + case 4: + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + break; + case 3: + color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); + break; + } + pixelRow[x] = color; } } } } - private void UncompressRle32(int width, int height, Span buffer) + private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; - var pixel = new byte[4]; + var pixel = new byte[bytesPerPixel]; int totalPixels = width * height; while (uncompressedPixels < totalPixels) { @@ -308,24 +260,24 @@ namespace SixLabors.ImageSharp.Formats.Tga if (highBit == 1) { int runLength = runLengthByte & 127; - this.currentStream.Read(pixel, 0, 4); - int bufferIdx = uncompressedPixels * 4; + 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 += 4; + bufferIdx += bytesPerPixel; } } else { // Non-run-length encoded packet. int runLength = runLengthByte; - int bufferIdx = uncompressedPixels * 4; + int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { - this.currentStream.Read(pixel, 0, 4); + this.currentStream.Read(pixel, 0, bytesPerPixel); pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); - bufferIdx += 4; + bufferIdx += bytesPerPixel; } } } From fabd7f4bbe30565628fea0b9ec69b586057e5494 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Oct 2019 20:53:49 +0200 Subject: [PATCH 10/40] Add decoding of 16 and 8 bit rle images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 78 ++++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 224a14131c..b6b078f6de 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -74,11 +74,28 @@ namespace SixLabors.ImageSharp.Formats.Tga switch (this.fileHeader.PixelDepth) { case 8: - this.ReadMonoChrome(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1); + } + else + { + this.ReadMonoChrome(pixels); + } + break; case 16: - this.ReadBgra16(pixels); + if (this.fileHeader.ImageType.IsRunLengthEncoded()) + { + long currentPosition = this.currentStream.Position; + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2); + } + else + { + this.ReadBgra16(pixels); + } + break; case 24: @@ -156,26 +173,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } // We need to set each alpha component value to fully opaque. - // Reset our stream for a second pass. - this.currentStream.Position = currentPosition; - for (int y = 0; y < this.fileHeader.Height; y++) - { - this.currentStream.Read(row); - PixelOperations.Instance.FromBgra5551Bytes( - this.configuration, - row.GetSpan(), - bgraRowSpan, - this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); - - for (int x = 0; x < this.fileHeader.Width; x++) - { - Bgra5551 bgra = bgraRowSpan[x]; - bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); - ref TPixel pixel = ref pixelSpan[x]; - pixel.FromBgra5551(bgra); - } - } + this.MakeOpaque(pixels, currentPosition, row, bgraRowSpan); } } @@ -232,12 +230,19 @@ namespace SixLabors.ImageSharp.Formats.Tga int idx = rowStartIdx + (x * bytesPerPixel); switch (bytesPerPixel) { - case 4: - color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + case 1: + color.FromGray8(Unsafe.As(ref bufferSpan[idx])); + break; + case 2: + 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; @@ -283,6 +288,31 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) + where TPixel : struct, IPixel + { + // Reset our stream for a second pass. + this.currentStream.Position = currentPosition; + for (int y = 0; y < this.fileHeader.Height; y++) + { + this.currentStream.Read(row); + PixelOperations.Instance.FromBgra5551Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + this.fileHeader.Width); + Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + + for (int x = 0; x < this.fileHeader.Width; x++) + { + Bgra5551 bgra = bgraRowSpan[x]; + bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); + ref TPixel pixel = ref pixelSpan[x]; + pixel.FromBgra5551(bgra); + } + } + } + /// /// Reads the raw image information from the specified stream. /// From 167eec1a165c28b7f98ec0db9250136a1d054488 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Oct 2019 19:30:25 +0200 Subject: [PATCH 11/40] Add support for encoding 24 bit tga files --- .../Formats/Tga/ITgaEncoderOptions.cs | 16 +++ src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs | 31 +++++ .../Formats/Tga/TgaConfigurationModule.cs | 1 + src/ImageSharp/Formats/Tga/TgaEncoder.cs | 26 ++++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 131 ++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 8 ++ src/ImageSharp/Formats/Tga/TgaMetadata.cs | 16 ++- 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaEncoder.cs create mode 100644 src/ImageSharp/Formats/Tga/TgaEncoderCore.cs diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs new file mode 100644 index 0000000000..a6e9871e0a --- /dev/null +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -0,0 +1,16 @@ +// 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; } + } +} 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/TgaConfigurationModule.cs b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs index c7bf6cc93d..18fbf4acd0 100644 --- a/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs +++ b/src/ImageSharp/Formats/Tga/TgaConfigurationModule.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// 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/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs new file mode 100644 index 0000000000..f5b9fa752c --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -0,0 +1,26 @@ +// 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 +{ + public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions + { + /// + /// Gets or sets the number of bits per pixel. + /// + public TgaBitsPerPixel? BitsPerPixel { get; set; } + + /// + 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..349963f7fb --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +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; + + /// + /// The color depth, in number of bits per pixel. + /// + private TgaBitsPerPixel? bitsPerPixel; + + /// + /// 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; + } + + 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; + + var fileHeader = new TgaFileHeader( + idLength: 0, + colorMapType: 0, + imageType: TgaImageType.TrueColor, + cMapStart: 0, + cMapLength: 0, + cMapDepth: 0, + xOffset: 0, + yOffset: 0, + width: (short)image.Width, + height: (short)image.Height, + pixelDepth: (byte)this.bitsPerPixel.Value, + imageDescriptor: 0); + +#if NETCOREAPP2_1 + Span buffer = stackalloc byte[TgaFileHeader.Size]; +#else + var buffer = new byte[TgaFileHeader.Size]; +#endif + fileHeader.WriteTo(buffer); + + stream.Write(buffer, 0, TgaFileHeader.Size); + + 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.Pixel24: + this.Write24Bit(stream, pixels); + break; + } + } + + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0); + + /// + /// 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()); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs index 51390e1b73..72c275b289 100644 --- a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Tga @@ -135,5 +136,12 @@ namespace SixLabors.ImageSharp.Formats.Tga { 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/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 185eaedc9a..4ce61d2e48 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -15,7 +15,21 @@ namespace SixLabors.ImageSharp.Formats.Tga { } + /// + /// 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() => throw new System.NotImplementedException(); + public IDeepCloneable DeepClone() => new TgaMetadata(this); } } From 98f4def9a918b6857d2f94969211c02772917957 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Oct 2019 19:51:28 +0200 Subject: [PATCH 12/40] Support for encoding 8, 16 and 32 bit tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 9 ++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 84 ++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index b6b078f6de..a9002fd8cf 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -20,6 +20,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private ImageMetadata metadata; + /// + /// The tga specific metadata. + /// + private TgaMetadata tgaMetadata; + /// /// The file header containing general information about the image. /// @@ -342,9 +347,9 @@ namespace SixLabors.ImageSharp.Formats.Tga #endif this.currentStream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); - - // TODO: no meta data yet. this.metadata = new ImageMetadata(); + this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); + this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 349963f7fb..ddd430b055 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -96,14 +96,74 @@ namespace SixLabors.ImageSharp.Formats.Tga 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; } } 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. /// @@ -127,5 +187,29 @@ namespace SixLabors.ImageSharp.Formats.Tga } } } + + /// + /// 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()); + } + } + } } } From 462ba485f58a3ecb5f4e6739bd6df2abd9bc0d20 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 10 Oct 2019 20:55:52 +0200 Subject: [PATCH 13/40] Add support for decoding tga image with palette --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 144 +++++++++++++++---- 1 file changed, 120 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index a9002fd8cf..12f81a2690 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -73,9 +73,47 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.ReadFileHeader(stream); + // TODO: parse ID + + // 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"); + } + + byte[] palette = null; + int colorMapPixelSizeInBytes = 0; + if (this.fileHeader.ColorMapType == 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"); + } + + colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; + this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); + } + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (this.fileHeader.ImageType == TgaImageType.ColorMapped) + { + if (palette is null) + { + TgaThrowHelper.ThrowImageFormatException("Tga image is missing a color palette"); + } + + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + return image; + } + switch (this.fileHeader.PixelDepth) { case 8: @@ -85,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadMonoChrome(pixels); + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -98,7 +136,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgra16(pixels); + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -110,7 +148,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgr24(pixels); + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -122,7 +160,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } else { - this.ReadBgra32(pixels); + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels); } break; @@ -140,41 +178,99 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadMonoChrome(Buffer2D pixels) + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int pixelSizeInBytes) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 1, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) { - for (int y = 0; y < this.fileHeader.Height; y++) + TPixel color = default; + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelRow = pixels.GetRowSpan(height - y - 1); + switch (pixelSizeInBytes) + { + case 1: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromGray8(Unsafe.As(ref palette[colorIndex])); + pixelRow[x] = color; + } + + break; + + case 2: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra5551(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 3: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgr24(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + + case 4: + for (int x = 0; x < width; x++) + { + int colorIndex = rowSpan[x]; + color.FromBgra32(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + pixelRow[x] = color; + } + + break; + } + } + } + } + + private void ReadMonoChrome(int width, int height, Buffer2D pixels) + where TPixel : struct, IPixel + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } - private void ReadBgra16(Buffer2D pixels) + private void ReadBgra16(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 2, 0)) - using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(this.fileHeader.Width)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) + using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width)) { Span bgraRowSpan = bgraRow.GetSpan(); long currentPosition = this.currentStream.Position; for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } // We need to set each alpha component value to fully opaque. @@ -182,38 +278,38 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgr24(Buffer2D pixels) + private void ReadBgr24(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 3, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) { - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } - private void ReadBgra32(Buffer2D pixels) + private void ReadBgra32(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { - using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(this.fileHeader.Width, 4, 0)) + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) { - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), pixelSpan, - this.fileHeader.Width); + width); } } } From dce04067d81e46e9f2084374a47ed97a5deb5f14 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 11 Oct 2019 19:55:59 +0200 Subject: [PATCH 14/40] Add support for decoding rle tga with palette --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 84 +++++++++++++------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 12f81a2690..aa6ccb0305 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -72,8 +72,7 @@ namespace SixLabors.ImageSharp.Formats.Tga try { this.ReadFileHeader(stream); - - // TODO: parse ID + this.currentStream.Skip(this.fileHeader.IdLength); // Parse the color map, if present. if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) @@ -81,9 +80,12 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } + var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + byte[] palette = null; int colorMapPixelSizeInBytes = 0; - if (this.fileHeader.ColorMapType == 1) + if (this.fileHeader.ColorMapType is 1) { if (this.fileHeader.CMapLength <= 0) { @@ -98,19 +100,16 @@ namespace SixLabors.ImageSharp.Formats.Tga colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); - } - var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); - - if (this.fileHeader.ImageType == TgaImageType.ColorMapped) - { - if (palette is null) + if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) + { + this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + } + else { - TgaThrowHelper.ThrowImageFormatException("Tga image is missing a color palette"); + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); } - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); return image; } @@ -128,6 +127,7 @@ namespace SixLabors.ImageSharp.Formats.Tga break; + case 15: case 16: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { @@ -178,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int pixelSizeInBytes) + private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) @@ -190,23 +190,13 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.currentStream.Read(row); Span pixelRow = pixels.GetRowSpan(height - y - 1); - switch (pixelSizeInBytes) + switch (colorMapPixelSizeInBytes) { - case 1: - for (int x = 0; x < width; x++) - { - int colorIndex = rowSpan[x]; - color.FromGray8(Unsafe.As(ref palette[colorIndex])); - pixelRow[x] = color; - } - - break; - case 2: for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra5551(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgra5551(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -216,7 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgr24(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -226,7 +216,7 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra32(Unsafe.As(ref palette[colorIndex * pixelSizeInBytes])); + color.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } @@ -236,6 +226,45 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + 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++) + { + Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + 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: + color.FromBgra5551(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + 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; + } + } + } + } + private void ReadMonoChrome(int width, int height, Buffer2D pixels) where TPixel : struct, IPixel { @@ -335,6 +364,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref bufferSpan[idx])); break; case 2: + // Set bit 16 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; From 296e75bb3f793572e0612221f951add158109c35 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 11 Oct 2019 21:27:08 +0200 Subject: [PATCH 15/40] Change test images, add additional tests --- .../Formats/Tga/TgaDecoderTests.cs | 60 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 21 ++++--- tests/Images/Input/Tga/bike_16bit.tga | 3 - tests/Images/Input/Tga/bike_16bit_rle.tga | 3 - tests/Images/Input/Tga/bike_24bit.tga | 3 - tests/Images/Input/Tga/bike_24bit_rle.tga | 3 - tests/Images/Input/Tga/bike_32bit.tga | 3 - tests/Images/Input/Tga/bike_32bit_rle.tga | 3 - tests/Images/Input/Tga/bike_8bit.tga | 3 - tests/Images/Input/Tga/bike_8bit_rle.tga | 3 - tests/Images/Input/Tga/ccm8.tga | 3 + tests/Images/Input/Tga/rgb15.tga | 3 + tests/Images/Input/Tga/rgb15rle.tga | 3 + tests/Images/Input/Tga/targa.png | 3 + tests/Images/Input/Tga/targa_16bit.tga | 3 + tests/Images/Input/Tga/targa_16bit_pal.tga | 3 + tests/Images/Input/Tga/targa_16bit_rle.tga | 3 + tests/Images/Input/Tga/targa_24bit.tga | 3 + tests/Images/Input/Tga/targa_24bit_pal.tga | 3 + tests/Images/Input/Tga/targa_24bit_rle.tga | 3 + tests/Images/Input/Tga/targa_32bit.tga | 3 + tests/Images/Input/Tga/targa_32bit_rle.tga | 3 + tests/Images/Input/Tga/targa_8bit.tga | 3 + tests/Images/Input/Tga/targa_8bit_rle.tga | 3 + 24 files changed, 115 insertions(+), 32 deletions(-) delete mode 100644 tests/Images/Input/Tga/bike_16bit.tga delete mode 100644 tests/Images/Input/Tga/bike_16bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_24bit.tga delete mode 100644 tests/Images/Input/Tga/bike_24bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_32bit.tga delete mode 100644 tests/Images/Input/Tga/bike_32bit_rle.tga delete mode 100644 tests/Images/Input/Tga/bike_8bit.tga delete mode 100644 tests/Images/Input/Tga/bike_8bit_rle.tga create mode 100644 tests/Images/Input/Tga/ccm8.tga create mode 100644 tests/Images/Input/Tga/rgb15.tga create mode 100644 tests/Images/Input/Tga/rgb15rle.tga create mode 100644 tests/Images/Input/Tga/targa.png create mode 100644 tests/Images/Input/Tga/targa_16bit.tga create mode 100644 tests/Images/Input/Tga/targa_16bit_pal.tga create mode 100644 tests/Images/Input/Tga/targa_16bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_24bit.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_pal.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_32bit.tga create mode 100644 tests/Images/Input/Tga/targa_32bit_rle.tga create mode 100644 tests/Images/Input/Tga/targa_8bit.tga create mode 100644 tests/Images/Input/Tga/targa_8bit_rle.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 47983a3ade..a2a50ead2d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -31,6 +31,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [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); + 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); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit16, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_16Bit(TestImageProvider provider) @@ -43,6 +67,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [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); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit24, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_24Bit(TestImageProvider provider) @@ -115,6 +151,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [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); + 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); + CompareWithReferenceDecoder(provider, image); + } + } + private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 51ca48c375..8499e51ebd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -368,14 +368,19 @@ namespace SixLabors.ImageSharp.Tests public static class Tga { - public const string Bit32 = "Tga/bike_32bit.tga"; - public const string Bit24 = "Tga/bike_24bit.tga"; - public const string Bit16 = "Tga/bike_16bit.tga"; - public const string Grey = "Tga/bike_8bit.tga"; - public const string Bit32Rle = "Tga/bike_32bit_rle.tga"; - public const string Bit24Rle = "Tga/bike_24bit_rle.tga"; - public const string Bit16Rle = "Tga/bike_16bit_rle.tga"; - public const string GreyRle = "Tga/bike_8bit_rle.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 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/Images/Input/Tga/bike_16bit.tga b/tests/Images/Input/Tga/bike_16bit.tga deleted file mode 100644 index 00489d94a0..0000000000 --- a/tests/Images/Input/Tga/bike_16bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b2bc922e2397ce8cd8b1e7792f20e2c7edad68ad8fac037a5b91bba5148a80b -size 97518 diff --git a/tests/Images/Input/Tga/bike_16bit_rle.tga b/tests/Images/Input/Tga/bike_16bit_rle.tga deleted file mode 100644 index 47c37c50b6..0000000000 --- a/tests/Images/Input/Tga/bike_16bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c9f7381f04b3ce23e3144f6b5789d7eccc0c0f010fc901f42d01171556181c1c -size 56938 diff --git a/tests/Images/Input/Tga/bike_24bit.tga b/tests/Images/Input/Tga/bike_24bit.tga deleted file mode 100644 index a5e46f794d..0000000000 --- a/tests/Images/Input/Tga/bike_24bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:26d55794c9b012b07517d1129630ccc35ca56015cbdd481debea00826130f925 -size 146268 diff --git a/tests/Images/Input/Tga/bike_24bit_rle.tga b/tests/Images/Input/Tga/bike_24bit_rle.tga deleted file mode 100644 index 65dfb9a1ab..0000000000 --- a/tests/Images/Input/Tga/bike_24bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88f7936db92cf536c9656a8ff00c25842c71b93823e05322a1efc3aef2c0a80e -size 106721 diff --git a/tests/Images/Input/Tga/bike_32bit.tga b/tests/Images/Input/Tga/bike_32bit.tga deleted file mode 100644 index 70e755d1f1..0000000000 --- a/tests/Images/Input/Tga/bike_32bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb3774b695d2409f3e7584dc67ce7b3d8a75377c194e7f33e4cc315b3ae36a35 -size 195018 diff --git a/tests/Images/Input/Tga/bike_32bit_rle.tga b/tests/Images/Input/Tga/bike_32bit_rle.tga deleted file mode 100644 index 967e9ce7ee..0000000000 --- a/tests/Images/Input/Tga/bike_32bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:815e9f32f29a8c51bbd0f2c7d507d6331a709f157fb7bd65188fd07677764c59 -size 141452 diff --git a/tests/Images/Input/Tga/bike_8bit.tga b/tests/Images/Input/Tga/bike_8bit.tga deleted file mode 100644 index ba11619a5e..0000000000 --- a/tests/Images/Input/Tga/bike_8bit.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75f3ce893f38d90767c692eb9628026c0421330934a5cbb686813ec26a9606d2 -size 48768 diff --git a/tests/Images/Input/Tga/bike_8bit_rle.tga b/tests/Images/Input/Tga/bike_8bit_rle.tga deleted file mode 100644 index 0c6ee06a44..0000000000 --- a/tests/Images/Input/Tga/bike_8bit_rle.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1531a0f8a95fb4a57e0bde1842b6ee65f5cabfc62ad883d1f9dbc2c5616e498f -size 37259 diff --git a/tests/Images/Input/Tga/ccm8.tga b/tests/Images/Input/Tga/ccm8.tga new file mode 100644 index 0000000000..ab92516355 --- /dev/null +++ b/tests/Images/Input/Tga/ccm8.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67b3ffaaa75561d8b959258d6b26a1f9ca3228b02a3df98a614ea43241aaea52 +size 9271 diff --git a/tests/Images/Input/Tga/rgb15.tga b/tests/Images/Input/Tga/rgb15.tga new file mode 100644 index 0000000000..870295b45a --- /dev/null +++ b/tests/Images/Input/Tga/rgb15.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390cfff190bc41386fa134eca70ea0d3ffdc32a285c73278ed34046b09c46c9d +size 80537 diff --git a/tests/Images/Input/Tga/rgb15rle.tga b/tests/Images/Input/Tga/rgb15rle.tga new file mode 100644 index 0000000000..a45940fc98 --- /dev/null +++ b/tests/Images/Input/Tga/rgb15rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3219186fc9a9f859c99c2b31cf81e7f0ab4292956d22fc659e714d0cdb51cfa7 +size 19941 diff --git a/tests/Images/Input/Tga/targa.png b/tests/Images/Input/Tga/targa.png new file mode 100644 index 0000000000..c4933c0ebd --- /dev/null +++ b/tests/Images/Input/Tga/targa.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abc382cec34a04815bd53ad30707c6cdeeceece8731244244e2ab91026d60957 +size 106139 diff --git a/tests/Images/Input/Tga/targa_16bit.tga b/tests/Images/Input/Tga/targa_16bit.tga new file mode 100644 index 0000000000..6c4143c2ee --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3adea897f8843b73d0042e23bdfbd0115a7f534df90699134e768df57061f46 +size 70518 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..b25def7798 --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit_pal.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a4ac0cecfe69e1b5c74db5288fb8ca3bf29968e3b5288c4e5ce03bb4f06915 +size 35780 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..49ef0e998b --- /dev/null +++ b/tests/Images/Input/Tga/targa_16bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47d7ebf37672ea846ce071155733697e34083de36aeaafaebd78317708feffde +size 19566 diff --git a/tests/Images/Input/Tga/targa_24bit.tga b/tests/Images/Input/Tga/targa_24bit.tga new file mode 100644 index 0000000000..82c22e2425 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35921b6250e43ba8e1fb125ebe4939a57a67efb0aa9eac0d3605bf90e93309b1 +size 105768 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..abfbf588a6 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_pal.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4926969e5ae6c9af38d33fa18429de756c48d06edd87c5d27cb8d5232b066ab2 +size 36036 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..d6af44c0a6 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56a79ab92d84bbe8c7efbc2711051938fa3ba97b48830aea0cb1dafd7d1fe222 +size 37711 diff --git a/tests/Images/Input/Tga/targa_32bit.tga b/tests/Images/Input/Tga/targa_32bit.tga new file mode 100644 index 0000000000..8b2a57c810 --- /dev/null +++ b/tests/Images/Input/Tga/targa_32bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3a220619e25e86bab01b01a2e231ee64fd004e047fa86016bf68de576877352 +size 141018 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..b021a2cc15 --- /dev/null +++ b/tests/Images/Input/Tga/targa_32bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f415d6a246909c18fe604248ab5fe27c74aff9a63df58d8cdeab7c4c3cbe056a +size 49994 diff --git a/tests/Images/Input/Tga/targa_8bit.tga b/tests/Images/Input/Tga/targa_8bit.tga new file mode 100644 index 0000000000..9b0512971e --- /dev/null +++ b/tests/Images/Input/Tga/targa_8bit.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aaae46d0e55f32a72732fbe48ed9dc4044c53432999ab66e9475e45e40f0133 +size 35268 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..d6a66def15 --- /dev/null +++ b/tests/Images/Input/Tga/targa_8bit_rle.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a18d7fd98bc9ab62276103b4e7b474be93b3d7241f4f06aa564e32150e205a71 +size 13145 From e12873c115330332980b2c093f57cf70d812d651 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 12 Oct 2019 21:23:44 +0200 Subject: [PATCH 16/40] Add tests for topleft origin --- .../Formats/Tga/TgaDecoderTests.cs | 24 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ .../Input/Tga/targa_24bit_origin_topleft.tga | 3 +++ .../Tga/targa_24bit_rle_origin_topleft.tga | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 tests/Images/Input/Tga/targa_24bit_origin_topleft.tga create mode 100644 tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index a2a50ead2d..e1b23a48e2 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -91,6 +91,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [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); + CompareWithReferenceDecoder(provider, image); + } + } + + [Theory] + [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_Uncompressed_WithTopLeftOrigin_24Bit(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new TgaDecoder())) + { + image.DebugSave(provider); + CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_32Bit(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8499e51ebd..e60219eed5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -373,6 +373,8 @@ namespace SixLabors.ImageSharp.Tests 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_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"; diff --git a/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga new file mode 100644 index 0000000000..b8c4071745 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1c52e538a7d134b20ff57e44b7e304d1b5effacac03a4481d169702796fb195 +size 36062 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..9310c51a70 --- /dev/null +++ b/tests/Images/Input/Tga/targa_24bit_rle_origin_topleft.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30e8b6d01ebf9d227d2e9dcdd7b2641bf8f335107110dfff780351870217d4f4 +size 37102 From deb1bf5284948bad4dcb0fb4a9d4f600326b1373 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 10:42:25 +0200 Subject: [PATCH 17/40] Add support for images with top left origin --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 102 ++++++++++-------- .../Formats/Tga/TgaDecoderTests.cs | 2 +- tests/ImageSharp.Tests/TestImages.cs | 2 +- ...tga => targa_24bit_pal_origin_topleft.tga} | 0 4 files changed, 61 insertions(+), 45 deletions(-) rename tests/Images/Input/Tga/{targa_24bit_origin_topleft.tga => targa_24bit_pal_origin_topleft.tga} (100%) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index aa6ccb0305..64635ea8a9 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { try { - this.ReadFileHeader(stream); + bool inverted = this.ReadFileHeader(stream); this.currentStream.Skip(this.fileHeader.IdLength); // Parse the color map, if present. @@ -83,8 +83,6 @@ namespace SixLabors.ImageSharp.Formats.Tga var image = new Image(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); - byte[] palette = null; - int colorMapPixelSizeInBytes = 0; if (this.fileHeader.ColorMapType is 1) { if (this.fileHeader.CMapLength <= 0) @@ -97,17 +95,17 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); } - colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; - palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; + int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; + var palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) { - this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); } else { - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes); + this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); } return image; @@ -118,11 +116,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 8: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); } else { - this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -132,11 +130,11 @@ namespace SixLabors.ImageSharp.Formats.Tga if (this.fileHeader.ImageType.IsRunLengthEncoded()) { long currentPosition = this.currentStream.Position; - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); } else { - this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -144,11 +142,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 24: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); } else { - this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -156,11 +154,11 @@ namespace SixLabors.ImageSharp.Formats.Tga case 32: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4); + this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); } else { - this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels); + this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; @@ -178,7 +176,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + 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)) @@ -189,7 +187,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelRow = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); switch (colorMapPixelSizeInBytes) { case 2: @@ -226,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes) + private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { int bytesPerPixel = 1; @@ -238,7 +237,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -265,7 +265,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadMonoChrome(int width, int height, Buffer2D pixels) + private void ReadMonoChrome(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) @@ -273,7 +273,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromGray8Bytes( this.configuration, row.GetSpan(), @@ -283,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgra16(int width, int height, Buffer2D pixels) + private void ReadBgra16(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) @@ -294,7 +295,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < this.fileHeader.Height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, row.GetSpan(), @@ -307,7 +309,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgr24(int width, int height, Buffer2D pixels) + private void ReadBgr24(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) @@ -315,7 +317,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), @@ -325,7 +328,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadBgra32(int width, int height, Buffer2D pixels) + private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) @@ -333,7 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int y = 0; y < height; y++) { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), @@ -343,7 +347,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel) + private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel, bool inverted) where TPixel : struct, IPixel { TPixel color = default; @@ -353,7 +357,8 @@ namespace SixLabors.ImageSharp.Formats.Tga this.UncompressRle(width, height, bufferSpan, bytesPerPixel); for (int y = 0; y < height; y++) { - Span pixelRow = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -382,6 +387,20 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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); + } + private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; @@ -444,25 +463,15 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - /// - /// 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); - } + [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. - private void ReadFileHeader(Stream stream) + /// true, if the image origin is top left. + private bool ReadFileHeader(Stream stream) { this.currentStream = stream; @@ -476,6 +485,13 @@ namespace SixLabors.ImageSharp.Formats.Tga this.metadata = new ImageMetadata(); this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; + + if (this.fileHeader.YOffset > 0) + { + return true; + } + + return false; } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index e1b23a48e2..54d94c7651 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -105,7 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit24TopLeft, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_Uncompressed_WithTopLeftOrigin_24Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_Palette_WithTopLeftOrigin_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e60219eed5..1777498694 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -373,7 +373,7 @@ namespace SixLabors.ImageSharp.Tests 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_origin_topleft.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"; diff --git a/tests/Images/Input/Tga/targa_24bit_origin_topleft.tga b/tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga similarity index 100% rename from tests/Images/Input/Tga/targa_24bit_origin_topleft.tga rename to tests/Images/Input/Tga/targa_24bit_pal_origin_topleft.tga From 20f1a1fa65618a8333e8610d82c8612b4ebda8cc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 11:23:34 +0200 Subject: [PATCH 18/40] Treat bgra5551 pixels as opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 64635ea8a9..eef4dc71b3 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -195,7 +195,11 @@ namespace SixLabors.ImageSharp.Formats.Tga for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; - color.FromBgra5551(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); + + // Set bit 16 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; } @@ -249,7 +253,10 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 2: - color.FromBgra5551(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + // Set bit 16 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])); From ca03cebdda88df6149504937213304cd82d3469b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 13:05:08 +0200 Subject: [PATCH 19/40] Add support for encoding RLE tga images --- .../Formats/Tga/ITgaEncoderOptions.cs | 5 ++ src/ImageSharp/Formats/Tga/TgaEncoder.cs | 8 +++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 63 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs index a6e9871e0a..ef1fecc93a 100644 --- a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -12,5 +12,10 @@ namespace SixLabors.ImageSharp.Formats.Tga /// Gets the number of bits per pixel. /// TgaBitsPerPixel? BitsPerPixel { get; } + + /// + /// Gets a value indicating whether run length compression should be used. + /// + bool Compress { get; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index f5b9fa752c..85b4fadfcd 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -8,6 +8,9 @@ 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 { /// @@ -15,6 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// public TgaBitsPerPixel? BitsPerPixel { get; set; } + /// + /// Gets or sets a value indicating whether run length compression should be used. + /// + public bool Compress { get; set; } + /// public void Encode(Image image, Stream stream) where TPixel : struct, IPixel diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index ddd430b055..b48c35e05f 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -32,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private TgaBitsPerPixel? bitsPerPixel; + /// + /// Indicates if run length compression should be used. + /// + private readonly bool useCompression; + /// /// Initializes a new instance of the class. /// @@ -41,6 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; + this.useCompression = options.Compress; } public void Encode(Image image, Stream stream) @@ -54,10 +60,11 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; + TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, - imageType: TgaImageType.TrueColor, + imageType: imageType, cMapStart: 0, cMapLength: 0, cMapDepth: 0, @@ -77,7 +84,14 @@ namespace SixLabors.ImageSharp.Formats.Tga stream.Write(buffer, 0, TgaFileHeader.Size); - this.WriteImage(stream, image.Frames.RootFrame); + if (this.useCompression) + { + this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); + } + else + { + this.WriteImage(stream, image.Frames.RootFrame); + } stream.Flush(); } @@ -114,6 +128,51 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + private void WriteRunLengthEndcodedImage(Stream stream, ImageFrame image) + where TPixel : struct, IPixel + { + Rgba32 color = default; + Buffer2D pixels = image.PixelBuffer; + Span pixelSpan = pixels.Span; + 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)); + stream.WriteByte((byte)(equalPixelCount | 128)); + stream.WriteByte(color.B); + stream.WriteByte(color.G); + stream.WriteByte(color.R); + encodedPixels += equalPixelCount + 1; + } + } + + 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); /// From 7b3018c3e191893fffaff38975ce7fc1c35ee95d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 14:26:14 +0200 Subject: [PATCH 20/40] Add support for encoding rle 8, 16 and bit tga images --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 61 +++++++++++++++- .../Formats/Tga/TgaEncoderTests.cs | 73 +++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index b48c35e05f..4a283260c5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -61,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Tga this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) + { + imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + } + var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, @@ -141,10 +148,38 @@ namespace SixLabors.ImageSharp.Formats.Tga 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)); - stream.WriteByte(color.B); - stream.WriteByte(color.G); - stream.WriteByte(color.R); + switch (this.bitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + int luminance = GetLuminance(currentPixel); + stream.WriteByte((byte)luminance); + break; + + case TgaBitsPerPixel.Pixel16: + // TODO: this seems to be wrong + var bgra5551 = new Bgra5551(color.ToVector4()); + stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF)); + stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF00)); + + 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; } } @@ -270,5 +305,25 @@ namespace SixLabors.ImageSharp.Formats.Tga } } } + + /// + /// 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 + { + Vector4 vector = sourcePixel.ToVector4(); + return GetLuminance(ref vector); + } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(ref Vector4 vector) + => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs new file mode 100644 index 0000000000..a92b50516d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +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 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() + { + Compress = true + }; + + 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); + } + } + } + } + } +} From 3b5af03f5fceecf62714718e16217763c1184d2e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:17:56 +0200 Subject: [PATCH 21/40] Add test for the tga encoder --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 2 +- .../Formats/Tga/TgaDecoderTests.cs | 74 ++++--------------- .../Formats/Tga/TgaEncoderTests.cs | 69 +++++++++++++++++ .../Formats/Tga/TgaTestUtils.cs | 51 +++++++++++++ 4 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 4a283260c5..291c61a814 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Formats.Tga cMapLength: 0, cMapDepth: 0, xOffset: 0, - yOffset: 0, + yOffset: this.useCompression ? (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, diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 54d94c7651..68a3fbe28a 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -1,15 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.IO; - -using ImageMagick; - -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -27,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -39,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -51,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -63,7 +56,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -87,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -99,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -111,7 +104,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -123,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -135,7 +128,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -147,7 +140,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -159,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -171,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -183,7 +176,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } @@ -195,44 +188,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga using (Image image = provider.GetImage(new TgaDecoder())) { image.DebugSave(provider); - CompareWithReferenceDecoder(provider, image); - } - } - - private void CompareWithReferenceDecoder(TestImageProvider provider, Image image) - 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 = this.DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); - ImageComparer.Exact.VerifySimilarity(image, magickImage); - } - - private 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; + TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index a92b50516d..4e1cea226d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -14,6 +14,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public class TgaEncoderTests { + public static readonly TheoryData BitsPerPixel = + new TheoryData + { + TgaBitsPerPixel.Pixel24, + TgaBitsPerPixel.Pixel32 + }; + public static readonly TheoryData TgaBitsPerPixelFiles = new TheoryData { @@ -69,5 +76,67 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } } + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void Encode_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + + private static void TestTgaEncoderCore( + TestImageProvider provider, + TgaBitsPerPixel bitsPerPixel, + bool useCompression = false) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compress = useCompression}; + + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + memStream.Position = 0; + using (var encodedImage = (Image)Image.Load(memStream)) + { + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage); + } + } + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs new file mode 100644 index 0000000000..f127322fda --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -0,0 +1,51 @@ +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) + 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)); + ImageComparer.Exact.VerifySimilarity(image, magickImage); + } + + 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; + } + } + } +} From 96d6afe944ab744c6be38a6cb8634da14b09df2e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:23:17 +0200 Subject: [PATCH 22/40] Skip palette bytes if image type indicates its no palette image --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index eef4dc71b3..7b7f803ca1 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -111,6 +111,13 @@ namespace SixLabors.ImageSharp.Formats.Tga 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: From ad50ab87189677457fe601529421a858667dd79a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 17:55:01 +0200 Subject: [PATCH 23/40] A little cleanup and comments --- src/ImageSharp/Formats/README.md | 6 ++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 90 +++++++++++++++++++ src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 18 ++++ src/ImageSharp/Formats/Tga/TgaImageType.cs | 21 ----- .../Formats/Tga/TgaImageTypeExtensions.cs | 26 ++++++ 5 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 src/ImageSharp/Formats/README.md create mode 100644 src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs 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/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 7b7f803ca1..8f36a7626b 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -50,6 +50,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private readonly ITgaDecoderOptions options; + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) { this.configuration = configuration; @@ -183,6 +188,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -236,6 +251,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -279,6 +304,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -298,6 +331,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -323,6 +364,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -342,6 +391,14 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -361,6 +418,15 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -415,6 +481,13 @@ namespace SixLabors.ImageSharp.Formats.Tga 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; @@ -452,6 +525,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// Helper method for decoding BGRA5551 images. Makes the pixels opaque, because the high bit does not + /// represent an alpha channel. + /// TODO: maybe there is a better/faster way to achieve this. + /// + /// The pixel type. + /// The destination pixel buffer. + /// The start position of pixel data. + /// A byte array to store the read pixel data. + /// Bgra pixel row span. private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) where TPixel : struct, IPixel { @@ -477,6 +560,13 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 291c61a814..1bde05c937 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -51,6 +51,12 @@ namespace SixLabors.ImageSharp.Formats.Tga this.useCompression = options.Compress; } + /// + /// 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 { @@ -135,6 +141,12 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { @@ -184,6 +196,12 @@ namespace SixLabors.ImageSharp.Formats.Tga } } + /// + /// 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 { diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index 2c19a06954..cf0eda93c4 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -46,25 +46,4 @@ namespace SixLabors. /// RleBlackAndWhite = 11, } - - /// - /// 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; - } - } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs new file mode 100644 index 0000000000..406e12d08b --- /dev/null +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -0,0 +1,26 @@ +// 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; + } + } +} From f612fabf7901c623ffba2cc136bd5b6acfd6c851 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 18:03:23 +0200 Subject: [PATCH 24/40] Add CompareToOriginal at the end of Issue1014 test --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (Image image = provider.GetImage(new PngDecoder())) { image.DebugSave(provider); - // TODO: compare to expected output + image.CompareToOriginal(provider, ImageComparer.Exact); } }); Assert.Null(ex); From 2e034af05c6343f9ec6f90321d541e06febd73b0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 18:03:49 +0200 Subject: [PATCH 25/40] Set expected default configuration count to 5 --- tests/ImageSharp.Tests/ConfigurationTests.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 6f68d04288..e029970f6f 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 @@ namespace SixLabors.ImageSharp.Tests 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, @@ -92,14 +94,13 @@ namespace SixLabors.ImageSharp.Tests [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] @@ -107,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests { Configuration config = Configuration.CreateDefaultInstance(); - Assert.Equal(4, config.ImageFormats.Count()); + Assert.Equal(expectedDefaultConfigurationCount, config.ImageFormats.Count()); } [Fact] @@ -117,4 +118,4 @@ namespace SixLabors.ImageSharp.Tests Assert.True(config.WorkingBufferSizeHintInBytes > 1024); } } -} \ No newline at end of file +} From da6ff8b3d6a3e09fac10b62a232f05008fb778e2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 13 Oct 2019 19:16:40 +0200 Subject: [PATCH 26/40] Fix orientation of RLE images --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 3 ++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 8f36a7626b..e3a09601aa 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -590,7 +590,8 @@ namespace SixLabors.ImageSharp.Formats.Tga this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; - if (this.fileHeader.YOffset > 0) + // Bit at position 3 of the descriptor indicates, that the origin is top left instead of bottom right. + if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) { return true; } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 1bde05c937..a441a40d82 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -74,6 +74,9 @@ namespace SixLabors.ImageSharp.Formats.Tga imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } + // If compression is used, set byte 3 of the image descriptor to indicate an left top origin. + byte imageDescriptor = (byte)(this.useCompression ? 32 : 0); + var fileHeader = new TgaFileHeader( idLength: 0, colorMapType: 0, @@ -86,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Tga width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, - imageDescriptor: 0); + imageDescriptor: (byte)(this.useCompression ? 32 : 0)); #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; @@ -327,7 +330,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// - /// The pixel to get the luminance from + /// The pixel to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(TPixel sourcePixel) where TPixel : struct, IPixel @@ -339,7 +342,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// - /// The vector to get the luminance from + /// The vector to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(ref Vector4 vector) => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); From c4413d5a55782b1844d9751c5d344a8d44382c79 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 18:32:47 +0200 Subject: [PATCH 27/40] Fix encoding of 16 tga files --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 9 ++++----- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 16 +++++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e3a09601aa..e1850a32b6 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -141,7 +141,6 @@ namespace SixLabors.ImageSharp.Formats.Tga case 16: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { - long currentPosition = this.currentStream.Position; this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); } else @@ -218,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { int colorIndex = rowSpan[x]; - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // 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); @@ -285,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 2: - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // 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); @@ -449,7 +448,7 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromGray8(Unsafe.As(ref bufferSpan[idx])); break; case 2: - // Set bit 16 to 1, to treat it as opaque for Bgra5551. + // 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; @@ -590,7 +589,7 @@ namespace SixLabors.ImageSharp.Formats.Tga this.tgaMetadata = this.metadata.GetFormatMetadata(TgaFormat.Instance); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; - // Bit at position 3 of the descriptor indicates, that the origin is top left instead of bottom right. + // 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; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index a441a40d82..0b0e0b1935 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.IO; using System.Numerics; using System.Runtime.CompilerServices; @@ -29,6 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private Configuration configuration; + /// + /// Reusable buffer for writing data. + /// + private readonly byte[] buffer = new byte[2]; + /// /// The color depth, in number of bits per pixel. /// @@ -74,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tga imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } - // If compression is used, set byte 3 of the image descriptor to indicate an left top origin. + // If compression is used, set bit 5 of the image descriptor to indicate an left top origin. byte imageDescriptor = (byte)(this.useCompression ? 32 : 0); var fileHeader = new TgaFileHeader( @@ -89,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Tga width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, - imageDescriptor: (byte)(this.useCompression ? 32 : 0)); + imageDescriptor: imageDescriptor); #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; @@ -174,10 +180,10 @@ namespace SixLabors.ImageSharp.Formats.Tga break; case TgaBitsPerPixel.Pixel16: - // TODO: this seems to be wrong var bgra5551 = new Bgra5551(color.ToVector4()); - stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF)); - stream.WriteByte((byte)(bgra5551.PackedValue & 0xFF00)); + BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); + stream.WriteByte(this.buffer[0]); + stream.WriteByte(this.buffer[1]); break; From f1a021b065aff7558f6a1af7c1b91a25093e1c71 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 20:18:19 +0200 Subject: [PATCH 28/40] Using tolerant comparer for 16 and 8 bit --- .../Formats/Tga/TgaDecoderTests.cs | 10 ++++++---- .../Formats/Tga/TgaEncoderTests.cs | 18 ++++++++++++------ .../Formats/Tga/TgaTestUtils.cs | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 68a3fbe28a..03ad10de40 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -1,6 +1,8 @@ // 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; @@ -122,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(GreyRle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_MonoChrome(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_MonoChrome(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -134,7 +136,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit16Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_16Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -146,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit24Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_24Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) @@ -158,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32Rle, PixelTypes.Rgba32)] - public void TgaDecoder_CanDecode_RunLenghtEncoded_32Bit(TestImageProvider provider) + public void TgaDecoder_CanDecode_RunLengthEncoded_32Bit(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new TgaDecoder())) diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 4e1cea226d..5dd49f4faa 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -1,6 +1,8 @@ // 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; @@ -80,12 +82,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel); + // 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, useCompression: true, 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); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: false, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] @@ -100,12 +103,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] public void Encode_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) - where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, true); + // 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, useCompression: true, 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, true); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, useCompression: true, useExactComparer: false); [Theory] [WithFile(Bit32, PixelTypes.Rgba32)] @@ -120,7 +124,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, - bool useCompression = false) + bool useCompression = false, + bool useExactComparer = true, + float compareTolerance = 0.01f) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -133,7 +139,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga memStream.Position = 0; using (var encodedImage = (Image)Image.Load(memStream)) { - TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage); + TgaTestUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs index f127322fda..a2f2e86d7d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -11,7 +11,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { public static class TgaTestUtils { - public static void CompareWithReferenceDecoder(TestImageProvider provider, Image image) + public static void CompareWithReferenceDecoder(TestImageProvider provider, + Image image, + bool useExactComparer = true, + float compareTolerance = 0.01f) where TPixel : struct, IPixel { string path = TestImageProvider.GetFilePathOrNull(provider); @@ -22,7 +25,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga TestFile testFile = TestFile.Create(path); Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); - ImageComparer.Exact.VerifySimilarity(image, magickImage); + if (useExactComparer) + { + ImageComparer.Exact.VerifySimilarity(magickImage, image); + } + else + { + ImageComparer.Tolerant(compareTolerance).VerifySimilarity(magickImage, image); + } } public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) From e4fcdd03c5e0ba705019f33f4e8603d384603211 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 26 Oct 2019 20:18:41 +0200 Subject: [PATCH 29/40] Add tga specification --- .../Formats/Tga/TGA_Specification.pdf | Bin 0 -> 181405 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TGA_Specification.pdf diff --git a/src/ImageSharp/Formats/Tga/TGA_Specification.pdf b/src/ImageSharp/Formats/Tga/TGA_Specification.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09c9a4dddade89630ba198f14a3298c00e190e40 GIT binary patch literal 181405 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�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 kP Date: Sat, 26 Oct 2019 21:22:25 +0200 Subject: [PATCH 30/40] Fix build error in Release mode --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 0b0e0b1935..139cc13fd5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -100,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Tga #if NETCOREAPP2_1 Span buffer = stackalloc byte[TgaFileHeader.Size]; #else - var buffer = new byte[TgaFileHeader.Size]; + byte[] buffer = new byte[TgaFileHeader.Size]; #endif fileHeader.WriteTo(buffer); @@ -161,7 +161,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Rgba32 color = default; Buffer2D pixels = image.PixelBuffer; - Span pixelSpan = pixels.Span; + Span pixelSpan = pixels.GetSpan(); int totalPixels = image.Width * image.Height; int encodedPixels = 0; while (encodedPixels < totalPixels) @@ -341,7 +341,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public static int GetLuminance(TPixel sourcePixel) where TPixel : struct, IPixel { - Vector4 vector = sourcePixel.ToVector4(); + var vector = sourcePixel.ToVector4(); return GetLuminance(ref vector); } From 340af921341289ef97792655fecfba1d24f50bc7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 12:07:52 +0100 Subject: [PATCH 31/40] Add check for valid tga image type in the format detector --- src/ImageSharp/Formats/Tga/TgaConstants.cs | 5 ++++ src/ImageSharp/Formats/Tga/TgaFileHeader.cs | 2 +- .../Formats/Tga/TgaImageFormatDetector.cs | 11 ++++++-- .../Formats/Tga/TgaImageTypeExtensions.cs | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaConstants.cs b/src/ImageSharp/Formats/Tga/TgaConstants.cs index 88c98b06a9..5aabe92a1d 100644 --- a/src/ImageSharp/Formats/Tga/TgaConstants.cs +++ b/src/ImageSharp/Formats/Tga/TgaConstants.cs @@ -16,5 +16,10 @@ namespace SixLabors.ImageSharp.Formats.Tga /// 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/TgaFileHeader.cs b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs index 72c275b289..e2bbb6fbd2 100644 --- a/src/ImageSharp/Formats/Tga/TgaFileHeader.cs +++ b/src/ImageSharp/Formats/Tga/TgaFileHeader.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Defines the size of the data structure in the targa file. /// - public const int Size = 18; + public const int Size = TgaConstants.FileHeaderLength; public TgaFileHeader( byte idLength, diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs index e305728473..5a0b0f44ca 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public sealed class TgaImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 18; + public int HeaderSize => TgaConstants.FileHeaderLength; /// public IImageFormat DetectFormat(ReadOnlySpan header) @@ -21,7 +21,14 @@ namespace SixLabors.ImageSharp.Formats.Tga private bool IsSupportedFileFormat(ReadOnlySpan header) { - return header.Length >= this.HeaderSize; + if (header.Length >= this.HeaderSize) + { + // There is no magick bytes in a tga file, so at least the image type in the header will be checked for a valid value. + var imageType = (TgaImageType)header[2]; + return imageType.IsValid(); + } + + return true; } } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs index 406e12d08b..38477f09f5 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -22,5 +22,30 @@ namespace SixLabors.ImageSharp.Formats.Tga 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) + { + byte imageTypeVal = (byte)imageType; + + switch (imageTypeVal) + { + case 0: + case 1: + case 2: + case 3: + case 9: + case 10: + case 11: + return true; + + default: + return false; + } + } } } From 862018228d0e519838ab70161397af3682f5f660 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 12:29:12 +0100 Subject: [PATCH 32/40] Add tests for bmp and tga header to throw UnknownImageFormatException when there is insufficient data --- .../Formats/Bmp/BmpFileHeaderTests.cs | 21 ++++++++++++++ .../Formats/Tga/TgaFileHeaderTests.cs | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 25cf29406e..1406867086 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -1,4 +1,10 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System; +using System.IO; + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using Xunit; @@ -6,6 +12,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { public class BmpFileHeaderTests { + private static readonly byte[] Data = BitConverter.GetBytes(BmpConstants.TypeMarkers.Bitmap); + + private MemoryStream Stream { get; } = new MemoryStream(Data); + [Fact] public void TestWrite() { @@ -17,5 +27,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal("AQACAAAAAwAAAAQAAAA=", Convert.ToBase64String(buffer)); } + + [Fact] + public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs new file mode 100644 index 0000000000..943144d61a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -0,0 +1,29 @@ +// 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, 0 }; + + private MemoryStream Stream { get; } = new MemoryStream(Data); + + [Fact] + public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + { + Assert.Throws(() => + { + using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) + { + } + }); + } + } +} From 7db0caaedd4ee1e78f73a5b958db7cf20e8cc1af Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Oct 2019 17:19:03 +0100 Subject: [PATCH 33/40] Throw ImageFormatException when width or height is 0 --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 5 +++++ .../Formats/Tga/TgaImageFormatDetector.cs | 8 +++++++- .../Formats/Bmp/BmpFileHeaderTests.cs | 16 +--------------- .../Formats/Tga/TgaFileHeaderTests.cs | 8 ++++++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e1850a32b6..2d70143351 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -85,6 +85,11 @@ namespace SixLabors.ImageSharp.Formats.Tga 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(); diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs index 5a0b0f44ca..bd9cfa900c 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs @@ -23,7 +23,13 @@ namespace SixLabors.ImageSharp.Formats.Tga { if (header.Length >= this.HeaderSize) { - // There is no magick bytes in a tga file, so at least the image type in the header will be checked for a valid value. + // 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(); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs index 1406867086..4c3fe31493 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpFileHeaderTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; @@ -12,10 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { public class BmpFileHeaderTests { - private static readonly byte[] Data = BitConverter.GetBytes(BmpConstants.TypeMarkers.Bitmap); - - private MemoryStream Stream { get; } = new MemoryStream(Data); - [Fact] public void TestWrite() { @@ -27,16 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal("AQACAAAAAwAAAAQAAAA=", Convert.ToBase64String(buffer)); } - - [Fact] - public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() - { - Assert.Throws(() => - { - using (Image.Load(Configuration.Default, this.Stream, out IImageFormat _)) - { - } - }); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs index 943144d61a..c227b79576 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -11,12 +11,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { public class TgaFileHeaderTests { - private static readonly byte[] Data = { 0, 0, 0 }; + private static readonly byte[] Data = { + 0, + 0, + 15 // invalid tga image type + }; private MemoryStream Stream { get; } = new MemoryStream(Data); [Fact] - public void ImageLoad_WithoutEnoughData_Throws_UnknownImageFormatException() + public void ImageLoad_WithInvalidImageType_Throws_UnknownImageFormatException() { Assert.Throws(() => { From 8b0eaf8dfdeb655767337be1237e85cad0c55dbc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 7 Nov 2019 20:49:12 +0100 Subject: [PATCH 34/40] Code review changes --- Directory.Build.targets | 2 +- .../Formats/Tga/ITgaEncoderOptions.cs | 2 +- src/ImageSharp/Formats/Tga/TgaCompression.cs | 21 ++++++++++++ src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 33 ++++++++++++++----- src/ImageSharp/Formats/Tga/TgaEncoder.cs | 4 +-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 21 +++++++----- src/ImageSharp/Formats/Tga/TgaImageType.cs | 1 - .../Formats/Tga/TgaEncoderTests.cs | 22 ++++++------- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 9 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 src/ImageSharp/Formats/Tga/TgaCompression.cs 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/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs index ef1fecc93a..49983d2369 100644 --- a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs @@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Gets a value indicating whether run length compression should be used. /// - bool Compress { get; } + TgaCompression Compression { get; } } } 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/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 2d70143351..c5a4df3f96 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -106,16 +106,31 @@ namespace SixLabors.ImageSharp.Formats.Tga } int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; - var palette = new byte[this.fileHeader.CMapLength * colorMapPixelSizeInBytes]; - this.currentStream.Read(palette, this.fileHeader.CMapStart, palette.Length); - - if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) - { - this.ReadPalettedRle(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); - } - else + int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; + using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) { - this.ReadPaletted(this.fileHeader.Width, this.fileHeader.Height, pixels, palette, colorMapPixelSizeInBytes, inverted); + 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; diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 85b4fadfcd..52a300f2ed 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Tga public TgaBitsPerPixel? BitsPerPixel { get; set; } /// - /// Gets or sets a value indicating whether run length compression should be used. + /// Gets or sets a value indicating whether no compression or run length compression should be used. /// - public bool Compress { get; set; } + public TgaCompression Compression { get; set; } /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 139cc13fd5..8022a0636c 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -43,7 +43,12 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Indicates if run length compression should be used. /// - private readonly bool useCompression; + private readonly TgaCompression compression; + + /// + /// Vector for converting pixel to gray value. + /// + private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); /// /// Initializes a new instance of the class. @@ -54,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; - this.useCompression = options.Compress; + this.compression = options.Compression; } /// @@ -74,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaMetadata tgaMetadata = metadata.GetFormatMetadata(TgaFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? tgaMetadata.BitsPerPixel; - TgaImageType imageType = this.useCompression ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; + TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) { - imageType = this.useCompression ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; + 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.useCompression ? 32 : 0); + byte imageDescriptor = (byte)(this.compression is TgaCompression.RunLength ? 32 : 0); var fileHeader = new TgaFileHeader( idLength: 0, @@ -91,7 +96,7 @@ namespace SixLabors.ImageSharp.Formats.Tga cMapLength: 0, cMapDepth: 0, xOffset: 0, - yOffset: this.useCompression ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. + 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, @@ -106,7 +111,7 @@ namespace SixLabors.ImageSharp.Formats.Tga stream.Write(buffer, 0, TgaFileHeader.Size); - if (this.useCompression) + if (this.compression is TgaCompression.RunLength) { this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); } @@ -351,6 +356,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The vector to get the luminance from. [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(ref Vector4 vector) - => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (256 - 1)); + => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (256 - 1)); } } diff --git a/src/ImageSharp/Formats/Tga/TgaImageType.cs b/src/ImageSharp/Formats/Tga/TgaImageType.cs index cf0eda93c4..491fd3ea77 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageType.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageType.cs @@ -12,7 +12,6 @@ namespace SixLabors. { /// /// No image data included. - /// Not sure what this is used for. /// NoImageData = 0, diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 5dd49f4faa..e946729a15 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga { var options = new TgaEncoder() { - Compress = true + Compression = TgaCompression.RunLength }; TestFile testFile = TestFile.Create(imagePath); @@ -83,55 +83,55 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga [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, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); + 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, useCompression: false, useExactComparer: false); + 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); + 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); + 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, useCompression: true, useExactComparer: false, compareTolerance: 0.03f); + 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, useCompression: true, useExactComparer: false); + 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, true); + 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, true); + where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, - bool useCompression = false, + 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, Compress = useCompression}; + var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression}; using (var memStream = new MemoryStream()) { diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 1f6b8b4d95..1ac5f8085a 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -14,7 +14,7 @@ - + From c964b94e0f5107561df7e1d1902c1c21c057b407 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 8 Nov 2019 20:49:26 +0100 Subject: [PATCH 35/40] Fix converting pixel to gray in histogram equalization --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 17 ++++++++++++++++- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 15 +-------------- ...qualizationSlidingWindowProcessor{TPixel}.cs | 4 ++-- .../HistogramEqualizationProcessor{TPixel}.cs | 11 +---------- 4 files changed, 20 insertions(+), 27 deletions(-) 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/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 8022a0636c..28b87e9857 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -45,11 +45,6 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private readonly TgaCompression compression; - /// - /// Vector for converting pixel to gray value. - /// - private static readonly Vector4 Bt709 = new Vector4(.2126f, .7152f, .0722f, 0.0f); - /// /// Initializes a new instance of the class. /// @@ -347,15 +342,7 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { var vector = sourcePixel.ToVector4(); - return GetLuminance(ref vector); + return ImageMaths.GetBT709Luminance(ref vector, 256); } - - /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. - /// - /// The vector to get the luminance from. - [MethodImpl(InliningOptions.ShortMethod)] - public static int GetLuminance(ref Vector4 vector) - => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (256 - 1)); } } 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 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { 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 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { 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 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization 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)); } } From 2acaea2ee4cfe27b065905dbe0a8819549681a66 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 8 Nov 2019 21:16:38 +0100 Subject: [PATCH 36/40] Add benchmarks for tga images --- src/ImageSharp/Formats/Tga/TgaEncoder.cs | 2 +- .../Formats/Tga/TgaImageTypeExtensions.cs | 18 +++---- .../ImageSharp.Benchmarks/Codecs/DecodeTga.cs | 42 +++++++++++++++ .../ImageSharp.Benchmarks/Codecs/EncodeTga.cs | 54 +++++++++++++++++++ .../ImageSharp.Benchmarks.csproj | 1 + 5 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Codecs/DecodeTga.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 52a300f2ed..2fcbb822f5 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Gets or sets a value indicating whether no compression or run length compression should be used. /// - public TgaCompression Compression { get; set; } + public TgaCompression Compression { get; set; } = TgaCompression.RunLength; /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs index 38477f09f5..6a30cdddd7 100644 --- a/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs +++ b/src/ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs @@ -30,17 +30,15 @@ namespace SixLabors.ImageSharp.Formats.Tga /// true, if its a valid tga image type. public static bool IsValid(this TgaImageType imageType) { - byte imageTypeVal = (byte)imageType; - - switch (imageTypeVal) + switch (imageType) { - case 0: - case 1: - case 2: - case 3: - case 9: - case 10: - case 11: + case TgaImageType.NoImageData: + case TgaImageType.ColorMapped: + case TgaImageType.TrueColor: + case TgaImageType.BlackAndWhite: + case TgaImageType.RleColorMapped: + case TgaImageType.RleTrueColor: + case TgaImageType.RleBlackAndWhite: return true; default: 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 @@ + From bd80c72a8212a42063a9e37b4972725dade75143 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 9 Nov 2019 11:27:09 +0100 Subject: [PATCH 37/40] Add width and height as parameter for makeopaque, change currentPosition to pixelDataStart --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index c5a4df3f96..aa830057e8 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -366,7 +366,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Span bgraRowSpan = bgraRow.GetSpan(); long currentPosition = this.currentStream.Position; - for (int y = 0; y < this.fileHeader.Height; y++) + for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); @@ -379,7 +379,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } // We need to set each alpha component value to fully opaque. - this.MakeOpaque(pixels, currentPosition, row, bgraRowSpan); + this.MakeOpaque(pixels, width, height, currentPosition, row, bgraRowSpan); } } @@ -551,15 +551,17 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// The pixel type. /// The destination pixel buffer. - /// The start position of pixel data. + /// The width of the image. + /// The height of the image. + /// The start position of pixel data. /// A byte array to store the read pixel data. /// Bgra pixel row span. - private void MakeOpaque(Buffer2D pixels, long currentPosition, IManagedByteBuffer row, Span bgraRowSpan) + private void MakeOpaque(Buffer2D pixels, int width, int height, long pixelDataStart, IManagedByteBuffer row, Span bgraRowSpan) where TPixel : struct, IPixel { // Reset our stream for a second pass. - this.currentStream.Position = currentPosition; - for (int y = 0; y < this.fileHeader.Height; y++) + this.currentStream.Position = pixelDataStart; + for (int y = 0; y < width; y++) { this.currentStream.Read(row); PixelOperations.Instance.FromBgra5551Bytes( @@ -567,9 +569,9 @@ namespace SixLabors.ImageSharp.Formats.Tga row.GetSpan(), bgraRowSpan, this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(this.fileHeader.Height - y - 1); + Span pixelSpan = pixels.GetRowSpan(height - y - 1); - for (int x = 0; x < this.fileHeader.Width; x++) + for (int x = 0; x < width; x++) { Bgra5551 bgra = bgraRowSpan[x]; bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); From 28570402ddc9ec8182724cd525b798fc42f52f33 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 9 Nov 2019 18:11:55 +0100 Subject: [PATCH 38/40] Avoid second iteration over the stream in ReadBgra16 to make it opaque --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 53 ++++---------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index aa830057e8..d861450e04 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -362,24 +362,26 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) - using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width)) { - Span bgraRowSpan = bgraRow.GetSpan(); - long currentPosition = this.currentStream.Position; 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, - row.GetSpan(), + rowSpan, pixelSpan, width); } - - // We need to set each alpha component value to fully opaque. - this.MakeOpaque(pixels, width, height, currentPosition, row, bgraRowSpan); } } @@ -544,43 +546,6 @@ namespace SixLabors.ImageSharp.Formats.Tga } } - /// - /// Helper method for decoding BGRA5551 images. Makes the pixels opaque, because the high bit does not - /// represent an alpha channel. - /// TODO: maybe there is a better/faster way to achieve this. - /// - /// The pixel type. - /// The destination pixel buffer. - /// The width of the image. - /// The height of the image. - /// The start position of pixel data. - /// A byte array to store the read pixel data. - /// Bgra pixel row span. - private void MakeOpaque(Buffer2D pixels, int width, int height, long pixelDataStart, IManagedByteBuffer row, Span bgraRowSpan) - where TPixel : struct, IPixel - { - // Reset our stream for a second pass. - this.currentStream.Position = pixelDataStart; - for (int y = 0; y < width; y++) - { - this.currentStream.Read(row); - PixelOperations.Instance.FromBgra5551Bytes( - this.configuration, - row.GetSpan(), - bgraRowSpan, - this.fileHeader.Width); - Span pixelSpan = pixels.GetRowSpan(height - y - 1); - - for (int x = 0; x < width; x++) - { - Bgra5551 bgra = bgraRowSpan[x]; - bgra.PackedValue = (ushort)(bgra.PackedValue | (1 << 15)); - ref TPixel pixel = ref pixelSpan[x]; - pixel.FromBgra5551(bgra); - } - } - } - /// /// Returns the y- value based on the given height. /// From 11cc8121fa8c66ce81e2d824eadc8db04fcf9e7f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Nov 2019 19:22:07 +0100 Subject: [PATCH 39/40] Update external for issue 984 testimages --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 563ec6f777..ca4cf8318f 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 563ec6f7774734ba39924174c8961705a1ea6fa2 +Subproject commit ca4cf8318fe4d09f0fc825686dcd477ebfb5e3e5 From 50bacca782af71e666628b346bb596a524de23b4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Nov 2019 20:27:54 +0100 Subject: [PATCH 40/40] Add GetBT709Luminance with vector test --- .../Helpers/ImageMathsTests.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 @@ namespace SixLabors.ImageSharp.Tests.Helpers 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 +}