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