diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 2249c86bf..c3b8526ce 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -1,7 +1,6 @@ // 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.PixelFormats; diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index ead004003..bba04f98a 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -12,6 +12,9 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tga { + /// + /// Performs the tga decoding operation. + /// internal sealed class TgaDecoderCore { /// @@ -49,6 +52,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// private readonly ITgaDecoderOptions options; + /// + /// Indicates whether there is a alpha channel present. + /// + private bool hasAlpha; + /// /// Initializes a new instance of the class. /// @@ -89,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Tga TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } - if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + if (this.fileHeader.Width is 0 || this.fileHeader.Height is 0) { throw new UnknownImageFormatException("Width or height cannot be 0"); } @@ -199,7 +207,7 @@ namespace SixLabors.ImageSharp.Formats.Tga break; default: - TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); + TgaThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of tga files."); break; } @@ -241,9 +249,13 @@ namespace SixLabors.ImageSharp.Formats.Tga { int colorIndex = rowSpan[x]; - // Set alpha value to 1, to treat it as opaque for Bgra5551. Bgra5551 bgra = Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes]); - bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + if (!this.hasAlpha) + { + // Set alpha value to 1, to treat it as opaque for Bgra5551. + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + } + color.FromBgra5551(bgra); pixelRow[x] = color; } @@ -291,6 +303,7 @@ namespace SixLabors.ImageSharp.Formats.Tga using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { TPixel color = default; + var alphaBits = this.tgaMetadata.AlphaChannelBits; Span bufferSpan = buffer.GetSpan(); this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1); @@ -308,16 +321,30 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromL8(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 2: - // Set alpha value to 1, to treat it as opaque for Bgra5551. + Bgra5551 bgra = Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]); - bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + if (!this.hasAlpha) + { + // Set alpha value to 1, to treat it as opaque for Bgra5551. + bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); + } + color.FromBgra5551(bgra); break; case 3: color.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 4: - color.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + if (this.hasAlpha) + { + color.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); + } + else + { + var alpha = alphaBits is 0 ? byte.MaxValue : bufferSpan[idx + 3]; + color.FromBgra32(new Bgra32(bufferSpan[idx + 2], bufferSpan[idx + 1], bufferSpan[idx], (byte)alpha)); + } + break; } @@ -345,11 +372,7 @@ namespace SixLabors.ImageSharp.Formats.Tga this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); - PixelOperations.Instance.FromL8Bytes( - this.configuration, - row.GetSpan(), - pixelSpan, - width); + PixelOperations.Instance.FromL8Bytes(this.configuration, row.GetSpan(), pixelSpan, width); } } } @@ -372,19 +395,18 @@ namespace SixLabors.ImageSharp.Formats.Tga 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) + if (!this.hasAlpha) { - rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); + // We need to set the alpha component value to fully opaque. + for (int x = 1; x < rowSpan.Length; x += 2) + { + rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); + } } int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); - PixelOperations.Instance.FromBgra5551Bytes( - this.configuration, - rowSpan, - pixelSpan, - width); + PixelOperations.Instance.FromBgra5551Bytes(this.configuration, rowSpan, pixelSpan, width); } } } @@ -407,11 +429,7 @@ namespace SixLabors.ImageSharp.Formats.Tga this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); - PixelOperations.Instance.FromBgr24Bytes( - this.configuration, - row.GetSpan(), - pixelSpan, - width); + PixelOperations.Instance.FromBgr24Bytes(this.configuration, row.GetSpan(), pixelSpan, width); } } } @@ -427,18 +445,41 @@ namespace SixLabors.ImageSharp.Formats.Tga private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) where TPixel : unmanaged, IPixel { + if (this.tgaMetadata.AlphaChannelBits is 8) + { + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) + { + for (int y = 0; y < height; y++) + { + this.currentStream.Read(row); + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + + PixelOperations.Instance.FromBgra32Bytes(this.configuration, row.GetSpan(), pixelSpan, width); + } + } + + return; + } + + TPixel color = default; + var alphaBits = this.tgaMetadata.AlphaChannelBits; using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) { for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); - Span pixelSpan = pixels.GetRowSpan(newY); - PixelOperations.Instance.FromBgra32Bytes( - this.configuration, - row.GetSpan(), - pixelSpan, - width); + Span pixelRow = pixels.GetRowSpan(newY); + Span rowSpan = row.GetSpan(); + + for (int x = 0; x < width; x++) + { + int idx = x * 4; + var alpha = alphaBits is 0 ? byte.MaxValue : rowSpan[idx + 3]; + color.FromBgra32(new Bgra32(rowSpan[idx + 2], rowSpan[idx + 1], rowSpan[idx], (byte)alpha)); + pixelRow[x] = color; + } } } } @@ -456,6 +497,7 @@ namespace SixLabors.ImageSharp.Formats.Tga where TPixel : unmanaged, IPixel { TPixel color = default; + var alphaBits = this.tgaMetadata.AlphaChannelBits; using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); @@ -474,15 +516,28 @@ namespace SixLabors.ImageSharp.Formats.Tga color.FromL8(Unsafe.As(ref bufferSpan[idx])); break; case 2: - // Set alpha value to 1, to treat it as opaque for Bgra5551. - bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); + if (!this.hasAlpha) + { + // Set alpha value to 1, to treat it as opaque for Bgra5551. + bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); + } + color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); break; case 3: color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); break; case 4: - color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + if (this.hasAlpha) + { + color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); + } + else + { + var alpha = alphaBits is 0 ? byte.MaxValue : bufferSpan[idx + 3]; + color.FromBgra32(new Bgra32(bufferSpan[idx + 2], bufferSpan[idx + 1], bufferSpan[idx], (byte)alpha)); + } + break; } @@ -524,7 +579,7 @@ namespace SixLabors.ImageSharp.Formats.Tga // The high bit of a run length packet is set to 1. int highBit = runLengthByte >> 7; - if (highBit == 1) + if (highBit is 1) { int runLength = runLengthByte & 127; this.currentStream.Read(pixel, 0, bytesPerPixel); @@ -577,7 +632,18 @@ namespace SixLabors.ImageSharp.Formats.Tga this.tgaMetadata = this.metadata.GetTgaMetadata(); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; - // Bit at position 5 of the descriptor indicates, that the origin is top left instead of bottom right. + var alphaBits = this.fileHeader.ImageDescriptor & 0xf; + if (alphaBits != 0 && alphaBits != 1 && alphaBits != 8) + { + TgaThrowHelper.ThrowImageFormatException("Invalid alpha channel bits"); + } + + this.tgaMetadata.AlphaChannelBits = (byte)alphaBits; + this.hasAlpha = alphaBits > 0; + + // TODO: bits 4 and 5 describe the image origin. See spec page 9. bit 4 is currently ignored. + // Theoretically the origin could also be top right and bottom right. + // Bit at position 5 of the descriptor indicates, that the origin is top left instead of bottom left. if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) { return true; diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 4ce61d2e4..69dee768a 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -29,6 +29,11 @@ namespace SixLabors.ImageSharp.Formats.Tga /// public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; + /// + /// Gets or sets the the number of alpha bits per pixel. + /// + public byte AlphaChannelBits { get; set; } = 0; + /// public IDeepCloneable DeepClone() => new TgaMetadata(this); } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index bcd98d714..ec2621e65 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -198,6 +198,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga } } + [Theory] + [WithFile(NoAlphaBits32Bit, PixelTypes.Rgba32)] + [WithFile(NoAlphaBits16Bit, PixelTypes.Rgba32)] + [WithFile(NoAlphaBits32BitRle, PixelTypes.Rgba32)] + [WithFile(NoAlphaBits16BitRle, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WhenAlphaBitsNotSet(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(TgaDecoder)) + { + image.DebugSave(provider); + TgaTestUtils.CompareWithReferenceDecoder(provider, image); + } + } + [Theory] [WithFile(Bit16, PixelTypes.Rgba32)] [WithFile(Bit24, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2e58ac970..db4c9a448 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -390,6 +390,10 @@ namespace SixLabors.ImageSharp.Tests 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"; + public const string NoAlphaBits16Bit = "Tga/16bit_noalphabits.tga"; + public const string NoAlphaBits16BitRle = "Tga/16bit_rle_noalphabits.tga"; + public const string NoAlphaBits32Bit = "Tga/32bit_noalphabits.tga"; + public const string NoAlphaBits32BitRle = "Tga/32bit_rle_no_alphabits.tga"; } } } diff --git a/tests/Images/Input/Tga/16bit_noalphabits.tga b/tests/Images/Input/Tga/16bit_noalphabits.tga new file mode 100644 index 000000000..0e97e86e3 Binary files /dev/null and b/tests/Images/Input/Tga/16bit_noalphabits.tga differ diff --git a/tests/Images/Input/Tga/16bit_rle_noalphabits.tga b/tests/Images/Input/Tga/16bit_rle_noalphabits.tga new file mode 100644 index 000000000..1f42f3de0 Binary files /dev/null and b/tests/Images/Input/Tga/16bit_rle_noalphabits.tga differ diff --git a/tests/Images/Input/Tga/32bit_noalphabits.tga b/tests/Images/Input/Tga/32bit_noalphabits.tga new file mode 100644 index 000000000..b239ca46d Binary files /dev/null and b/tests/Images/Input/Tga/32bit_noalphabits.tga differ diff --git a/tests/Images/Input/Tga/32bit_rle_no_alphabits.tga b/tests/Images/Input/Tga/32bit_rle_no_alphabits.tga new file mode 100644 index 000000000..2c12d52da Binary files /dev/null and b/tests/Images/Input/Tga/32bit_rle_no_alphabits.tga differ