📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

802 lines
32 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using static System.Net.Mime.MediaTypeNames;
namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary>
/// Image encoder for writing an image to a stream as a Windows bitmap.
/// </summary>
internal sealed class BmpEncoderCore : IImageEncoderInternals
{
/// <summary>
/// The amount to pad each row by.
/// </summary>
private int padding;
/// <summary>
/// The mask for the alpha channel of the color for 32 bit rgba bitmaps.
/// </summary>
private const int Rgba32AlphaMask = 0xFF << 24;
/// <summary>
/// The mask for the red part of the color for 32 bit rgba bitmaps.
/// </summary>
private const int Rgba32RedMask = 0xFF << 16;
/// <summary>
/// The mask for the green part of the color for 32 bit rgba bitmaps.
/// </summary>
private const int Rgba32GreenMask = 0xFF << 8;
/// <summary>
/// The mask for the blue part of the color for 32 bit rgba bitmaps.
/// </summary>
private const int Rgba32BlueMask = 0xFF;
/// <summary>
/// The color palette for an 8 bit image will have 256 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize8Bit = 1024;
/// <summary>
/// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize4Bit = 64;
/// <summary>
/// The color palette for an 2 bit image will have 4 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize2Bit = 16;
/// <summary>
/// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize1Bit = 8;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The color depth, in number of bits per pixel.
/// </summary>
private BmpBitsPerPixel? bitsPerPixel;
/// <summary>
/// A bitmap v4 header will only be written, if the user explicitly wants support for transparency.
/// In this case the compression type BITFIELDS will be used.
/// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info.
/// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders.
/// </summary>
private BmpInfoHeaderType infoHeaderType;
/// <summary>
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// The pixel sampling strategy for quantization.
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask;
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
private readonly bool skipFileHeader;
/// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
private readonly bool isDoubleHeight;
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader;
this.isDoubleHeight = encoder.UseDoubleHeight;
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
Configuration configuration = image.Configuration;
ImageMetadata metadata = image.Metadata;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
ushort bpp = (ushort)this.bitsPerPixel;
int bytesPerLine = (int)(4 * ((((uint)image.Width * bpp) + 31) / 32));
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
int colorPaletteSize = this.bitsPerPixel switch
{
BmpBitsPerPixel.Pixel8 => ColorPaletteSize8Bit,
BmpBitsPerPixel.Pixel4 => ColorPaletteSize4Bit,
BmpBitsPerPixel.Pixel2 => ColorPaletteSize2Bit,
BmpBitsPerPixel.Pixel1 => ColorPaletteSize1Bit,
_ => 0
};
byte[]? iccProfileData = null;
int iccProfileSize = 0;
if (metadata.IccProfile != null)
{
this.infoHeaderType = BmpInfoHeaderType.WinVersion5;
iccProfileData = metadata.IccProfile.ToByteArray();
iccProfileSize = iccProfileData.Length;
}
int infoHeaderSize = this.infoHeaderType switch
{
BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3,
BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4,
BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5,
_ => BmpInfoHeader.SizeV3
};
// for ico/cur encoder.
int height = image.Height;
if (this.isDoubleHeight)
{
height <<= 1;
}
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
// for ico/cur encoder.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image);
WriteColorProfile(stream, iccProfileData, buffer);
stream.Flush();
}
/// <summary>
/// Creates the bitmap information header.
/// </summary>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
/// <param name="bpp">The bits per pixel.</param>
/// <param name="bytesPerLine">The bytes per line.</param>
/// <param name="metadata">The metadata.</param>
/// <param name="iccProfileData">The icc profile data.</param>
/// <returns>The bitmap information header.</returns>
private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, ushort bpp, int bytesPerLine, ImageMetadata metadata, byte[]? iccProfileData)
{
int hResolution = 0;
int vResolution = 0;
if (metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio
&& metadata.HorizontalResolution > 0
&& metadata.VerticalResolution > 0)
{
switch (metadata.ResolutionUnits)
{
case PixelResolutionUnit.PixelsPerInch:
hResolution = (int)Math.Round(UnitConverter.InchToMeter(metadata.HorizontalResolution));
vResolution = (int)Math.Round(UnitConverter.InchToMeter(metadata.VerticalResolution));
break;
case PixelResolutionUnit.PixelsPerCentimeter:
hResolution = (int)Math.Round(UnitConverter.CmToMeter(metadata.HorizontalResolution));
vResolution = (int)Math.Round(UnitConverter.CmToMeter(metadata.VerticalResolution));
break;
case PixelResolutionUnit.PixelsPerMeter:
hResolution = (int)Math.Round(metadata.HorizontalResolution);
vResolution = (int)Math.Round(metadata.VerticalResolution);
break;
}
}
BmpInfoHeader infoHeader = new(
headerSize: infoHeaderSize,
width: width,
height: height,
planes: 1,
bitsPerPixel: bpp,
imageSize: height * bytesPerLine,
xPelsPerMeter: hResolution,
yPelsPerMeter: vResolution,
clrUsed: 0,
clrImportant: 0);
if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
{
infoHeader.AlphaMask = Rgba32AlphaMask;
infoHeader.RedMask = Rgba32RedMask;
infoHeader.GreenMask = Rgba32GreenMask;
infoHeader.BlueMask = Rgba32BlueMask;
infoHeader.Compression = BmpCompression.BitFields;
}
if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && iccProfileData != null)
{
infoHeader.ProfileSize = iccProfileData.Length;
infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED;
infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES;
}
return infoHeader;
}
/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileData">The color profile data.</param>
/// <param name="buffer">The buffer.</param>
private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span<byte> buffer)
{
if (iccProfileData != null)
{
// The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size;
stream.Write(iccProfileData);
BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData);
stream.Position = BmpFileHeader.Size + 112;
stream.Write(buffer[..4]);
}
}
/// <summary>
/// Writes the bitmap file header.
/// </summary>
/// <param name="stream">The stream to write the header to.</param>
/// <param name="infoHeaderSize">Size of the bitmap information header.</param>
/// <param name="colorPaletteSize">Size of the color palette.</param>
/// <param name="iccProfileSize">The size in bytes of the color profile.</param>
/// <param name="infoHeader">The information header to write.</param>
/// <param name="buffer">The buffer to write to.</param>
private static void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span<byte> buffer)
{
BmpFileHeader fileHeader = new(
type: BmpConstants.TypeMarkers.Bitmap,
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize,
reserved: 0,
offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize);
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, BmpFileHeader.Size);
}
/// <summary>
/// Writes the bitmap information header.
/// </summary>
/// <param name="stream">The stream to write info header into.</param>
/// <param name="infoHeader">The information header.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span<byte> buffer, int infoHeaderSize)
{
switch (this.infoHeaderType)
{
case BmpInfoHeaderType.WinVersion3:
infoHeader.WriteV3Header(buffer);
break;
case BmpInfoHeaderType.WinVersion4:
infoHeader.WriteV4Header(buffer);
break;
case BmpInfoHeaderType.WinVersion5:
infoHeader.WriteV5Header(buffer);
break;
}
stream.Write(buffer, 0, infoHeaderSize);
}
/// <summary>
/// Writes the pixel data to the binary stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WriteImage<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel)
{
case BmpBitsPerPixel.Pixel32:
this.Write32BitPixelData(configuration, stream, pixels);
break;
case BmpBitsPerPixel.Pixel24:
this.Write24BitPixelData(configuration, stream, pixels);
break;
case BmpBitsPerPixel.Pixel16:
this.Write16BitPixelData(configuration, stream, pixels);
break;
case BmpBitsPerPixel.Pixel8:
this.Write8BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Pixel4:
this.Write4BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Pixel2:
this.Write2BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Pixel1:
this.Write1BitPixelData(configuration, stream, image);
break;
}
}
private IMemoryOwner<byte> AllocateRow(int width, int bytesPerPixel)
=> this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding);
/// <summary>
/// Writes 32-bit data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
Span<byte> rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration,
pixelSpan,
rowSpan,
pixelSpan.Length);
stream.Write(rowSpan);
}
}
/// <summary>
/// Writes 24-bit pixel data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
int rowBytesWithoutPadding = width * 3;
using IMemoryOwner<byte> row = this.AllocateRow(width, 3);
Span<byte> rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration,
pixelSpan,
row.Slice(0, rowBytesWithoutPadding),
width);
stream.Write(rowSpan);
}
}
/// <summary>
/// Writes 16-bit pixel data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
int rowBytesWithoutPadding = width * 2;
using IMemoryOwner<byte> row = this.AllocateRow(width, 2);
Span<byte> rowSpan = row.GetSpan();
for (int y = pixels.Height - 1; y >= 0; y--)
{
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
configuration,
pixelSpan,
row.Slice(0, rowBytesWithoutPadding),
pixelSpan.Length);
stream.Write(rowSpan);
}
}
/// <summary>
/// Writes 8 bit pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
bool isL8 = typeof(TPixel) == typeof(L8);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
if (isL8)
{
this.Write8BitPixelData(stream, image, colorPalette);
}
else
{
this.Write8BitColor(configuration, stream, image, colorPalette);
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, image);
}
}
/// <summary>
/// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitColor<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<byte> pixelSpan = quantized.DangerousGetRowSpan(y);
stream.Write(pixelSpan);
for (int i = 0; i < this.padding; i++)
{
stream.WriteByte(0);
}
}
}
/// <summary>
/// Writes 8 bit gray pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
// Create a color palette with 256 different gray values.
for (int i = 0; i <= 255; i++)
{
int idx = i * 4;
byte grayValue = (byte)i;
colorPalette[idx] = grayValue;
colorPalette[idx + 1] = grayValue;
colorPalette[idx + 2] = grayValue;
// Padding byte, always 0.
colorPalette[idx + 3] = 0;
}
stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer();
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
stream.Write(outputPixelRow);
for (int i = 0; i < this.padding; i++)
{
stream.WriteByte(0);
}
}
}
/// <summary>
/// Writes 4 bit pixel data with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{
MaxColors = 16
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
{
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
for (int i = 0; i < endIdx; i += 2)
{
stream.WriteByte((byte)((pixelRowSpan[i] << 4) | pixelRowSpan[i + 1]));
}
if (pixelRowSpan.Length % 2 != 0)
{
stream.WriteByte((byte)((pixelRowSpan[^1] << 4) | 0));
}
for (int i = 0; i < rowPadding; i++)
{
stream.WriteByte(0);
}
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, image);
}
}
/// <summary>
/// Writes 2 bit pixel data with a color palette. The color palette has 4 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{
MaxColors = 4
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
{
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4;
int i = 0;
for (i = 0; i < endIdx; i += 4)
{
stream.WriteByte((byte)((pixelRowSpan[i] << 6) | (pixelRowSpan[i + 1] << 4) | (pixelRowSpan[i + 2] << 2) | pixelRowSpan[i + 3]));
}
if (pixelRowSpan.Length % 4 != 0)
{
int shift = 6;
byte pixelData = 0;
for (; i < pixelRowSpan.Length; i++)
{
pixelData = (byte)(pixelData | (pixelRowSpan[i] << shift));
shift -= 2;
}
stream.WriteByte(pixelData);
}
for (i = 0; i < rowPadding; i++)
{
stream.WriteByte(0);
}
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, image);
}
}
/// <summary>
/// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{
MaxColors = 2
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
{
quantizedPixelRow = quantized.DangerousGetRowSpan(y);
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
for (int i = 0; i < endIdx; i += 8)
{
Write1BitPalette(stream, i, i + 8, quantizedPixelRow);
}
if (quantizedPixelRow.Length % 8 != 0)
{
int startIdx = quantizedPixelRow.Length - (quantizedPixelRow.Length % 8);
endIdx = quantizedPixelRow.Length;
Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow);
}
for (int i = 0; i < rowPadding; i++)
{
stream.WriteByte(0);
}
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, image);
}
}
/// <summary>
/// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="quantizedColorPalette">The color palette from the quantized image.</param>
/// <param name="colorPalette">A temporary byte span to write the color palette to.</param>
private static void WriteColorPalette<TPixel>(Configuration configuration, Stream stream, ReadOnlySpan<TPixel> quantizedColorPalette, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
int quantizedColorBytes = quantizedColorPalette.Length * 4;
PixelOperations<TPixel>.Instance.ToBgra32(configuration, quantizedColorPalette, MemoryMarshal.Cast<byte, Bgra32>(colorPalette[..quantizedColorBytes]));
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
for (int i = 0; i < colorPaletteAsUInt.Length; i++)
{
colorPaletteAsUInt[i] &= 0x00FFFFFF; // Padding byte, always 0.
}
stream.Write(colorPalette);
}
/// <summary>
/// Writes a 1-bit palette.
/// </summary>
/// <param name="stream">The stream to write the palette to.</param>
/// <param name="startIdx">The start index.</param>
/// <param name="endIdx">The end index.</param>
/// <param name="quantizedPixelRow">A quantized pixel row.</param>
private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan<byte> quantizedPixelRow)
{
int shift = 7;
byte indices = 0;
for (int j = startIdx; j < endIdx; j++)
{
indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift));
shift--;
}
stream.WriteByte(indices);
}
private static void ProcessedAlphaMask<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = default;
int arrayWidth = image.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
padding = 4 - padding;
}
Span<byte> mask = stackalloc byte[arrayWidth];
for (int y = image.Height - 1; y >= 0; y--)
{
mask.Clear();
for (int x = 0; x < image.Width; x++)
{
int bit = x % 8;
int i = x / 8;
TPixel pixel = image[x, y];
pixel.ToRgba32(ref rgba);
if (rgba.A is not 0)
{
mask[i] &= unchecked((byte)(0b10000000 >> bit));
}
}
stream.Write(mask);
}
}
}