// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Tga { internal sealed class TgaDecoderCore { /// /// The metadata. /// private ImageMetadata metadata; /// /// The tga specific metadata. /// private TgaMetadata tgaMetadata; /// /// The file header containing general information about the image. /// private TgaFileHeader fileHeader; /// /// The global configuration. /// private readonly Configuration configuration; /// /// Used for allocating memory during processing operations. /// private readonly MemoryAllocator memoryAllocator; /// /// The stream to decode from. /// private Stream currentStream; /// /// The bitmap decoder options. /// private readonly ITgaDecoderOptions options; /// /// Initializes a new instance of the class. /// /// The configuration. /// The options. public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) { this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.options = options; } /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The stream, where the image should be decoded from. Cannot be null. /// /// is null. /// /// The decoded image. public Image Decode(Stream stream) where TPixel : struct, IPixel { try { bool inverted = this.ReadFileHeader(stream); this.currentStream.Skip(this.fileHeader.IdLength); // Parse the color map, if present. if (this.fileHeader.ColorMapType != 0 && this.fileHeader.ColorMapType != 1) { TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) { throw new UnknownImageFormatException("Width or height cannot be 0"); } var image = Image.CreateUninitialized(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.fileHeader.ColorMapType is 1) { if (this.fileHeader.CMapLength <= 0) { TgaThrowHelper.ThrowImageFormatException("Missing tga color map length"); } if (this.fileHeader.CMapDepth <= 0) { TgaThrowHelper.ThrowImageFormatException("Missing tga color map depth"); } int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; int colorMapSizeInBytes = this.fileHeader.CMapLength * colorMapPixelSizeInBytes; using (IManagedByteBuffer palette = this.memoryAllocator.AllocateManagedByteBuffer(colorMapSizeInBytes, AllocationOptions.Clean)) { this.currentStream.Read(palette.Array, this.fileHeader.CMapStart, colorMapSizeInBytes); if (this.fileHeader.ImageType is TgaImageType.RleColorMapped) { this.ReadPalettedRle( this.fileHeader.Width, this.fileHeader.Height, pixels, palette.Array, colorMapPixelSizeInBytes, inverted); } else { this.ReadPaletted( this.fileHeader.Width, this.fileHeader.Height, pixels, palette.Array, colorMapPixelSizeInBytes, inverted); } } return image; } // Even if the image type indicates it is not a paletted image, it can still contain a palette. Skip those bytes. if (this.fileHeader.CMapLength > 0) { int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; this.currentStream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); } switch (this.fileHeader.PixelDepth) { case 8: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, inverted); } else { this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; case 15: case 16: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, inverted); } else { this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; case 24: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, inverted); } else { this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; case 32: if (this.fileHeader.ImageType.IsRunLengthEncoded()) { this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, inverted); } else { this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, inverted); } break; default: TgaThrowHelper.ThrowNotSupportedException("Does not support this kind of tga files."); break; } return image; } catch (IndexOutOfRangeException e) { throw new ImageFormatException("TGA image does not have a valid format.", e); } } /// /// Reads a uncompressed TGA image with a palette. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// The color palette. /// Color map size of one entry in bytes. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadPaletted(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(width, AllocationOptions.Clean)) { TPixel color = default; Span rowSpan = row.GetSpan(); for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelRow = pixels.GetRowSpan(newY); switch (colorMapPixelSizeInBytes) { case 2: for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; // Set alpha value to 1, to treat it as opaque for Bgra5551. Bgra5551 bgra = Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes]); bgra.PackedValue = (ushort)(bgra.PackedValue | 0x8000); color.FromBgra5551(bgra); pixelRow[x] = color; } break; case 3: for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; color.FromBgr24(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } break; case 4: for (int x = 0; x < width; x++) { int colorIndex = rowSpan[x]; color.FromBgra32(Unsafe.As(ref palette[colorIndex * colorMapPixelSizeInBytes])); pixelRow[x] = color; } break; } } } } /// /// Reads a run length encoded TGA image with a palette. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// The color palette. /// Color map size of one entry in bytes. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadPalettedRle(int width, int height, Buffer2D pixels, byte[] palette, int colorMapPixelSizeInBytes, bool inverted) where TPixel : struct, IPixel { int bytesPerPixel = 1; using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { TPixel color = default; Span bufferSpan = buffer.GetSpan(); this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1); for (int y = 0; y < height; y++) { int newY = Invert(y, height, inverted); Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { int idx = rowStartIdx + x; switch (colorMapPixelSizeInBytes) { case 1: color.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); color.FromBgra5551(bgra); break; case 3: color.FromBgr24(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; case 4: color.FromBgra32(Unsafe.As(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); break; } pixelRow[x] = color; } } } } /// /// Reads a uncompressed monochrome TGA image. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadMonoChrome(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 1, 0)) { for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromL8Bytes( this.configuration, row.GetSpan(), pixelSpan, width); } } } /// /// Reads a uncompressed TGA image where each pixels has 16 bit. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgra16(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0)) { for (int y = 0; y < height; y++) { this.currentStream.Read(row); Span rowSpan = row.GetSpan(); // We need to set each alpha component value to fully opaque. for (int x = 1; x < rowSpan.Length; x += 2) { rowSpan[x] = (byte)(rowSpan[x] | (1 << 7)); } int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra5551Bytes( this.configuration, rowSpan, pixelSpan, width); } } } /// /// Reads a uncompressed TGA image where each pixels has 24 bit. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgr24(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, 0)) { for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row.GetSpan(), pixelSpan, width); } } } /// /// Reads a uncompressed TGA image where each pixels has 32 bit. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadBgra32(int width, int height, Buffer2D pixels, bool inverted) where TPixel : struct, IPixel { using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0)) { for (int y = 0; y < height; y++) { this.currentStream.Read(row); int newY = Invert(y, height, inverted); Span pixelSpan = pixels.GetRowSpan(newY); PixelOperations.Instance.FromBgra32Bytes( this.configuration, row.GetSpan(), pixelSpan, width); } } } /// /// Reads a run length encoded TGA image. /// /// The pixel type. /// The width of the image. /// The height of the image. /// The to assign the palette to. /// The bytes per pixel. /// Indicates, if the origin of the image is top left rather the bottom left (the default). private void ReadRle(int width, int height, Buffer2D pixels, int bytesPerPixel, bool inverted) where TPixel : struct, IPixel { TPixel color = default; using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * bytesPerPixel, AllocationOptions.Clean)) { Span bufferSpan = buffer.GetSpan(); this.UncompressRle(width, height, bufferSpan, bytesPerPixel); for (int y = 0; y < height; y++) { int newY = Invert(y, height, inverted); Span pixelRow = pixels.GetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { int idx = rowStartIdx + (x * bytesPerPixel); switch (bytesPerPixel) { case 1: color.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); color.FromBgra5551(Unsafe.As(ref bufferSpan[idx])); break; case 3: color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); break; case 4: color.FromBgra32(Unsafe.As(ref bufferSpan[idx])); break; } pixelRow[x] = color; } } } } /// /// Reads the raw image information from the specified stream. /// /// The containing image data. public IImageInfo Identify(Stream stream) { this.ReadFileHeader(stream); return new ImageInfo( new PixelTypeInfo(this.fileHeader.PixelDepth), this.fileHeader.Width, this.fileHeader.Height, this.metadata); } /// /// Produce uncompressed tga data from a run length encoded stream. /// /// The width of the image. /// The height of the image. /// Buffer for uncompressed data. /// The bytes used per pixel. private void UncompressRle(int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; var pixel = new byte[bytesPerPixel]; int totalPixels = width * height; while (uncompressedPixels < totalPixels) { byte runLengthByte = (byte)this.currentStream.ReadByte(); // The high bit of a run length packet is set to 1. int highBit = runLengthByte >> 7; if (highBit == 1) { int runLength = runLengthByte & 127; this.currentStream.Read(pixel, 0, bytesPerPixel); int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); bufferIdx += bytesPerPixel; } } else { // Non-run-length encoded packet. int runLength = runLengthByte; int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { this.currentStream.Read(pixel, 0, bytesPerPixel); pixel.AsSpan().CopyTo(buffer.Slice(bufferIdx)); bufferIdx += bytesPerPixel; } } } } /// /// Returns the y- value based on the given height. /// /// The y- value representing the current row. /// The height of the bitmap. /// Whether the bitmap is inverted. /// The representing the inverted value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; /// /// Reads the tga file header from the stream. /// /// The containing image data. /// true, if the image origin is top left. private bool ReadFileHeader(Stream stream) { this.currentStream = stream; Span buffer = stackalloc byte[TgaFileHeader.Size]; this.currentStream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); this.metadata = new ImageMetadata(); 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. if ((this.fileHeader.ImageDescriptor & (1 << 5)) != 0) { return true; } return false; } } }