// 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;
}
}
}