// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
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 color depth, in number of bits per pixel.
///
private TgaBitsPerPixel? bitsPerPixel;
///
/// Indicates if run length compression should be used.
///
private readonly TgaCompression compression;
private readonly TransparentColorMode transparentColorMode;
///
/// Initializes a new instance of the class.
///
/// The encoder with options.
/// The memory manager.
public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = encoder.Compression;
this.transparentColorMode = encoder.TransparentColorMode;
}
///
/// Encodes the image to the specified stream from the .
///
/// The pixel format.
/// The to encode from.
/// The to encode the image data to.
/// The token to request cancellation.
public void Encode(Image image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
ImageMetadata metadata = image.Metadata;
TgaMetadata tgaMetadata = metadata.GetTgaMetadata();
this.bitsPerPixel ??= tgaMetadata.BitsPerPixel;
TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor;
if (this.bitsPerPixel == TgaBitsPerPixel.Bit8)
{
imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite;
}
byte imageDescriptor = 0;
if (this.compression is TgaCompression.RunLength)
{
// If compression is used, set bit 5 of the image descriptor to indicate a left top origin.
imageDescriptor |= 0x20;
}
if (this.bitsPerPixel is TgaBitsPerPixel.Bit32)
{
// Indicate, that 8 bit are used for the alpha channel.
imageDescriptor |= 0x8;
}
if (this.bitsPerPixel is TgaBitsPerPixel.Bit16)
{
// Indicate, that 1 bit is used for the alpha channel.
imageDescriptor |= 0x1;
}
TgaFileHeader fileHeader = new(
idLength: 0,
colorMapType: 0,
imageType: imageType,
cMapStart: 0,
cMapLength: 0,
cMapDepth: 0,
xOffset: 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,
width: (short)image.Width,
height: (short)image.Height,
pixelDepth: (byte)this.bitsPerPixel.Value,
imageDescriptor: imageDescriptor);
Span buffer = stackalloc byte[TgaFileHeader.Size];
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, TgaFileHeader.Size);
ImageFrame? clonedFrame = null;
try
{
// TODO: Try to avoid cloning the frame if possible.
// We should be cloning individual scanlines instead.
if (EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ReplaceTransparentPixels(clonedFrame);
}
ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame;
if (this.compression is TgaCompression.RunLength)
{
this.WriteRunLengthEncodedImage(stream, encodingFrame, cancellationToken);
}
else
{
this.WriteImage(image.Configuration, stream, encodingFrame, cancellationToken);
}
stream.Flush();
}
finally
{
clonedFrame?.Dispose();
}
}
///
/// Writes the pixel data to the binary stream.
///
/// The pixel format.
/// The global configuration.
/// The to write to.
/// /// The containing pixel data.
/// The token to request cancellation.
private void WriteImage(Configuration configuration, Stream stream, ImageFrame image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Buffer2D pixels = image.PixelBuffer;
switch (this.bitsPerPixel)
{
case TgaBitsPerPixel.Bit8:
this.Write8Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Bit16:
this.Write16Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Bit24:
this.Write24Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Bit32:
this.Write32Bit(configuration, stream, pixels, cancellationToken);
break;
}
}
///
/// Writes a run length encoded tga image to the stream.
///
/// The pixel type.
/// The stream to write the image to.
/// The image to encode.
/// The token to request cancellation.
private void WriteRunLengthEncodedImage(Stream stream, ImageFrame image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Buffer2D pixels = image.PixelBuffer;
using IMemoryOwner rgbaOwner = this.memoryAllocator.Allocate(image.Width);
Span rgbaRow = rgbaOwner.GetSpan();
for (int y = 0; y < image.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span pixelRow = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgba32(image.Configuration, pixelRow, rgbaRow);
for (int x = 0; x < image.Width;)
{
TPixel currentPixel = pixelRow[x];
Rgba32 rgba = rgbaRow[x];
byte equalPixelCount = FindEqualPixels(pixelRow, x);
if (equalPixelCount > 0)
{
// Write the number of equal pixels, with the high bit set, indicating it's a compressed pixel run.
stream.WriteByte((byte)(equalPixelCount | 128));
this.WritePixel(stream, rgba);
x += equalPixelCount + 1;
}
else
{
// Write Raw Packet (i.e., Non-Run-Length Encoded):
byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x);
stream.WriteByte(unEqualPixelCount);
this.WritePixel(stream, rgba);
x++;
for (int i = 0; i < unEqualPixelCount; i++)
{
currentPixel = pixelRow[x];
rgba = rgbaRow[x];
this.WritePixel(stream, rgba);
x++;
}
}
}
}
}
///
/// Writes a the pixel to the stream.
///
/// The stream to write to.
/// The color of the pixel to write.
private void WritePixel(Stream stream, Rgba32 color)
{
switch (this.bitsPerPixel)
{
case TgaBitsPerPixel.Bit8:
L8 l8 = L8.FromRgba32(color);
stream.WriteByte(l8.PackedValue);
break;
case TgaBitsPerPixel.Bit16:
Bgra5551 bgra5551 = Bgra5551.FromRgba32(color);
Span buffer = stackalloc byte[2];
BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue);
stream.WriteByte(buffer[0]);
stream.WriteByte(buffer[1]);
break;
case TgaBitsPerPixel.Bit24:
stream.WriteByte(color.B);
stream.WriteByte(color.G);
stream.WriteByte(color.R);
break;
case TgaBitsPerPixel.Bit32:
stream.WriteByte(color.B);
stream.WriteByte(color.G);
stream.WriteByte(color.R);
stream.WriteByte(color.A);
break;
}
}
///
/// Finds consecutive pixels which have the same value up to 128 pixels maximum.
///
/// The pixel type.
/// A pixel row of the image to encode.
/// X coordinate to start searching for the same pixels.
/// The number of equal pixels.
private static byte FindEqualPixels(Span pixelRow, int xStart)
where TPixel : unmanaged, IPixel
{
byte equalPixelCount = 0;
TPixel startPixel = pixelRow[xStart];
for (int x = xStart + 1; x < pixelRow.Length; x++)
{
TPixel nextPixel = pixelRow[x];
if (startPixel.Equals(nextPixel))
{
equalPixelCount++;
}
else
{
return equalPixelCount;
}
if (equalPixelCount >= 127)
{
return equalPixelCount;
}
}
return equalPixelCount;
}
///
/// Finds consecutive pixels which are unequal up to 128 pixels maximum.
///
/// The pixel type.
/// A pixel row of the image to encode.
/// X coordinate to start searching for the unequal pixels.
/// The number of equal pixels.
private static byte FindUnEqualPixels(Span pixelRow, int xStart)
where TPixel : unmanaged, IPixel
{
byte unEqualPixelCount = 0;
TPixel currentPixel = pixelRow[xStart];
for (int x = xStart + 1; x < pixelRow.Length; x++)
{
TPixel nextPixel = pixelRow[x];
if (currentPixel.Equals(nextPixel))
{
return unEqualPixelCount;
}
unEqualPixelCount++;
if (unEqualPixelCount >= 127)
{
return unEqualPixelCount;
}
currentPixel = nextPixel;
}
return unEqualPixelCount;
}
private IMemoryOwner AllocateRow(int width, int bytesPerPixel)
=> this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, 0);
///
/// Writes the 8bit pixels uncompressed to the stream.
///
/// The pixel format.
/// The global configuration.
/// The to write to.
/// The containing pixel data.
/// The token to request cancellation.
private void Write8Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IMemoryOwner row = this.AllocateRow(pixels.Width, 1);
Span rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL8Bytes(
configuration,
pixelSpan,
rowSpan,
pixelSpan.Length);
stream.Write(rowSpan);
}
}
///
/// Writes the 16bit pixels uncompressed to the stream.
///
/// The pixel format.
/// The global configuration.
/// The to write to.
/// The containing pixel data.
/// The token to request cancellation.
private void Write16Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IMemoryOwner row = this.AllocateRow(pixels.Width, 2);
Span rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgra5551Bytes(
configuration,
pixelSpan,
rowSpan,
pixelSpan.Length);
stream.Write(rowSpan);
}
}
///
/// Writes the 24bit pixels uncompressed to the stream.
///
/// The pixel format.
/// The global configuration.
/// The to write to.
/// The containing pixel data.
/// The token to request cancellation.
private void Write24Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IMemoryOwner row = this.AllocateRow(pixels.Width, 3);
Span rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgr24Bytes(
configuration,
pixelSpan,
rowSpan,
pixelSpan.Length);
stream.Write(rowSpan);
}
}
///
/// Writes the 32bit pixels uncompressed to the stream.
///
/// The pixel format.
/// The global configuration.
/// The to write to.
/// The containing pixel data.
/// The token to request cancellation.
private void Write32Bit(Configuration configuration, Stream stream, Buffer2D pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IMemoryOwner row = this.AllocateRow(pixels.Width, 4);
Span rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgra32Bytes(
configuration,
pixelSpan,
rowSpan,
pixelSpan.Length);
stream.Write(rowSpan);
}
}
}