mirror of https://github.com/SixLabors/ImageSharp
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.
1487 lines
60 KiB
1487 lines
60 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
#nullable disable
|
|
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using SixLabors.ImageSharp.Common.Helpers;
|
|
using SixLabors.ImageSharp.IO;
|
|
using SixLabors.ImageSharp.Memory;
|
|
using SixLabors.ImageSharp.Metadata;
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
|
|
namespace SixLabors.ImageSharp.Formats.Bmp;
|
|
|
|
/// <summary>
|
|
/// Performs the bitmap decoding operation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A useful decoding source example can be found at <see href="https://dxr.mozilla.org/mozilla-central/source/image/decoders/nsBMPDecoder.cpp"/>
|
|
/// </remarks>
|
|
internal sealed class BmpDecoderCore : IImageDecoderInternals
|
|
{
|
|
/// <summary>
|
|
/// The default mask for the red part of the color for 16 bit rgb bitmaps.
|
|
/// </summary>
|
|
private const int DefaultRgb16RMask = 0x7C00;
|
|
|
|
/// <summary>
|
|
/// The default mask for the green part of the color for 16 bit rgb bitmaps.
|
|
/// </summary>
|
|
private const int DefaultRgb16GMask = 0x3E0;
|
|
|
|
/// <summary>
|
|
/// The default mask for the blue part of the color for 16 bit rgb bitmaps.
|
|
/// </summary>
|
|
private const int DefaultRgb16BMask = 0x1F;
|
|
|
|
/// <summary>
|
|
/// RLE flag value that indicates following byte has special meaning.
|
|
/// </summary>
|
|
private const int RleCommand = 0x00;
|
|
|
|
/// <summary>
|
|
/// RLE flag value marking end of a scan line.
|
|
/// </summary>
|
|
private const int RleEndOfLine = 0x00;
|
|
|
|
/// <summary>
|
|
/// RLE flag value marking end of bitmap data.
|
|
/// </summary>
|
|
private const int RleEndOfBitmap = 0x01;
|
|
|
|
/// <summary>
|
|
/// RLE flag value marking the start of [x,y] offset instruction.
|
|
/// </summary>
|
|
private const int RleDelta = 0x02;
|
|
|
|
/// <summary>
|
|
/// The stream to decode from.
|
|
/// </summary>
|
|
private BufferedReadStream stream;
|
|
|
|
/// <summary>
|
|
/// The metadata.
|
|
/// </summary>
|
|
private ImageMetadata metadata;
|
|
|
|
/// <summary>
|
|
/// The bitmap specific metadata.
|
|
/// </summary>
|
|
private BmpMetadata bmpMetadata;
|
|
|
|
/// <summary>
|
|
/// The file header containing general information.
|
|
/// </summary>
|
|
private BmpFileHeader fileHeader;
|
|
|
|
/// <summary>
|
|
/// Indicates which bitmap file marker was read.
|
|
/// </summary>
|
|
private BmpFileMarkerType fileMarkerType;
|
|
|
|
/// <summary>
|
|
/// The info header containing detailed information about the bitmap.
|
|
/// </summary>
|
|
private BmpInfoHeader infoHeader;
|
|
|
|
/// <summary>
|
|
/// The global configuration.
|
|
/// </summary>
|
|
private readonly Configuration configuration;
|
|
|
|
/// <summary>
|
|
/// Used for allocating memory during processing operations.
|
|
/// </summary>
|
|
private readonly MemoryAllocator memoryAllocator;
|
|
|
|
/// <summary>
|
|
/// How to deal with skipped pixels,
|
|
/// which can occur during decoding run length encoded bitmaps.
|
|
/// </summary>
|
|
private readonly RleSkippedPixelHandling rleSkippedPixelHandling;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="BmpDecoderCore"/> class.
|
|
/// </summary>
|
|
/// <param name="options">The options.</param>
|
|
public BmpDecoderCore(BmpDecoderOptions options)
|
|
{
|
|
this.Options = options.GeneralOptions;
|
|
this.rleSkippedPixelHandling = options.RleSkippedPixelHandling;
|
|
this.configuration = options.GeneralOptions.Configuration;
|
|
this.memoryAllocator = this.configuration.MemoryAllocator;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public DecoderOptions Options { get; }
|
|
|
|
/// <inheritdoc />
|
|
public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height);
|
|
|
|
/// <inheritdoc />
|
|
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
Image<TPixel> image = null;
|
|
try
|
|
{
|
|
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
|
|
|
|
image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
|
|
|
|
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
|
|
|
|
switch (this.infoHeader.Compression)
|
|
{
|
|
case BmpCompression.RGB:
|
|
if (this.infoHeader.BitsPerPixel == 32)
|
|
{
|
|
if (this.bmpMetadata.InfoHeaderType == BmpInfoHeaderType.WinVersion3)
|
|
{
|
|
this.ReadRgb32Slow(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
}
|
|
else
|
|
{
|
|
this.ReadRgb32Fast(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
}
|
|
}
|
|
else if (this.infoHeader.BitsPerPixel == 24)
|
|
{
|
|
this.ReadRgb24(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
}
|
|
else if (this.infoHeader.BitsPerPixel == 16)
|
|
{
|
|
this.ReadRgb16(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
}
|
|
else if (this.infoHeader.BitsPerPixel <= 8)
|
|
{
|
|
this.ReadRgbPalette(
|
|
pixels,
|
|
palette,
|
|
this.infoHeader.Width,
|
|
this.infoHeader.Height,
|
|
this.infoHeader.BitsPerPixel,
|
|
bytesPerColorMapEntry,
|
|
inverted);
|
|
}
|
|
|
|
break;
|
|
|
|
case BmpCompression.RLE24:
|
|
this.ReadRle24(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
|
|
break;
|
|
|
|
case BmpCompression.RLE8:
|
|
case BmpCompression.RLE4:
|
|
this.ReadRle(this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted);
|
|
|
|
break;
|
|
|
|
case BmpCompression.BitFields:
|
|
case BmpCompression.BI_ALPHABITFIELDS:
|
|
this.ReadBitFields(pixels, inverted);
|
|
|
|
break;
|
|
|
|
default:
|
|
BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files.");
|
|
|
|
break;
|
|
}
|
|
|
|
return image;
|
|
}
|
|
catch (IndexOutOfRangeException e)
|
|
{
|
|
image?.Dispose();
|
|
throw new ImageFormatException("Bitmap does not have a valid format.", e);
|
|
}
|
|
catch
|
|
{
|
|
image?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
|
|
{
|
|
this.ReadImageHeaders(stream, out _, out _);
|
|
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the y- value based on the given height.
|
|
/// </summary>
|
|
/// <param name="y">The y- value representing the current row.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
/// <returns>The <see cref="int"/> representing the inverted value.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y;
|
|
|
|
/// <summary>
|
|
/// Calculates the amount of bytes to pad a row.
|
|
/// </summary>
|
|
/// <param name="width">The image width.</param>
|
|
/// <param name="componentCount">The pixel component count.</param>
|
|
/// <returns>
|
|
/// The padding.
|
|
/// </returns>
|
|
private static int CalculatePadding(int width, int componentCount)
|
|
{
|
|
int padding = (width * componentCount) % 4;
|
|
|
|
if (padding != 0)
|
|
{
|
|
padding = 4 - padding;
|
|
}
|
|
|
|
return padding;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes a bitmap containing the BITFIELDS Compression type. For each color channel, there will be a bitmask
|
|
/// which will be used to determine which bits belong to that channel.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The output pixel buffer containing the decoded image.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadBitFields<TPixel>(Buffer2D<TPixel> pixels, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
if (this.infoHeader.BitsPerPixel == 16)
|
|
{
|
|
this.ReadRgb16(
|
|
pixels,
|
|
this.infoHeader.Width,
|
|
this.infoHeader.Height,
|
|
inverted,
|
|
this.infoHeader.RedMask,
|
|
this.infoHeader.GreenMask,
|
|
this.infoHeader.BlueMask);
|
|
}
|
|
else
|
|
{
|
|
this.ReadRgb32BitFields(
|
|
pixels,
|
|
this.infoHeader.Width,
|
|
this.infoHeader.Height,
|
|
inverted,
|
|
this.infoHeader.RedMask,
|
|
this.infoHeader.GreenMask,
|
|
this.infoHeader.BlueMask,
|
|
this.infoHeader.AlphaMask);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data.
|
|
/// Compressed RLE8 stream is uncompressed by <see cref="UncompressRle8(int, Span{byte}, Span{bool}, Span{bool})"/>
|
|
/// Compressed RLE4 stream is uncompressed by <see cref="UncompressRle4(int, Span{byte}, Span{bool}, Span{bool})"/>
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="compression">The compression type. Either RLE4 or RLE8.</param>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRle<TPixel>(BmpCompression compression, Buffer2D<TPixel> pixels, byte[] colors, int width, int height, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
TPixel color = default;
|
|
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height, AllocationOptions.Clean))
|
|
using (IMemoryOwner<bool> undefinedPixels = this.memoryAllocator.Allocate<bool>(width * height, AllocationOptions.Clean))
|
|
using (IMemoryOwner<bool> rowsWithUndefinedPixels = this.memoryAllocator.Allocate<bool>(height, AllocationOptions.Clean))
|
|
{
|
|
Span<bool> rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span;
|
|
Span<bool> undefinedPixelsSpan = undefinedPixels.Memory.Span;
|
|
Span<byte> bufferSpan = buffer.Memory.Span;
|
|
if (compression is BmpCompression.RLE8)
|
|
{
|
|
this.UncompressRle8(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
|
|
}
|
|
else
|
|
{
|
|
this.UncompressRle4(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
|
|
}
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
int newY = Invert(y, height, inverted);
|
|
int rowStartIdx = y * width;
|
|
Span<byte> bufferRow = bufferSpan.Slice(rowStartIdx, width);
|
|
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
|
|
|
|
bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y];
|
|
if (rowHasUndefinedPixels)
|
|
{
|
|
// Slow path with undefined pixels.
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
byte colorIdx = bufferRow[x];
|
|
if (undefinedPixelsSpan[rowStartIdx + x])
|
|
{
|
|
switch (this.rleSkippedPixelHandling)
|
|
{
|
|
case RleSkippedPixelHandling.FirstColorOfPalette:
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIdx * 4]));
|
|
break;
|
|
case RleSkippedPixelHandling.Transparent:
|
|
color.FromScaledVector4(Vector4.Zero);
|
|
break;
|
|
|
|
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
|
|
default:
|
|
color.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIdx * 4]));
|
|
}
|
|
|
|
pixelRow[x] = color;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fast path without any undefined pixels.
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[bufferRow[x] * 4]));
|
|
pixelRow[x] = color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up color values and builds the image from de-compressed RLE24.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRle24<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
TPixel color = default;
|
|
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height * 3, AllocationOptions.Clean))
|
|
using (IMemoryOwner<bool> undefinedPixels = this.memoryAllocator.Allocate<bool>(width * height, AllocationOptions.Clean))
|
|
using (IMemoryOwner<bool> rowsWithUndefinedPixels = this.memoryAllocator.Allocate<bool>(height, AllocationOptions.Clean))
|
|
{
|
|
Span<bool> rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span;
|
|
Span<bool> undefinedPixelsSpan = undefinedPixels.Memory.Span;
|
|
Span<byte> bufferSpan = buffer.GetSpan();
|
|
|
|
this.UncompressRle24(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
|
|
bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y];
|
|
if (rowHasUndefinedPixels)
|
|
{
|
|
// Slow path with undefined pixels.
|
|
int yMulWidth = y * width;
|
|
int rowStartIdx = yMulWidth * 3;
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int idx = rowStartIdx + (x * 3);
|
|
if (undefinedPixelsSpan[yMulWidth + x])
|
|
{
|
|
switch (this.rleSkippedPixelHandling)
|
|
{
|
|
case RleSkippedPixelHandling.FirstColorOfPalette:
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx]));
|
|
break;
|
|
case RleSkippedPixelHandling.Transparent:
|
|
color.FromScaledVector4(Vector4.Zero);
|
|
break;
|
|
|
|
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
|
|
default:
|
|
color.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx]));
|
|
}
|
|
|
|
pixelRow[x] = color;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fast path without any undefined pixels.
|
|
int rowStartIdx = y * width * 3;
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int idx = rowStartIdx + (x * 3);
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx]));
|
|
pixelRow[x] = color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Produce uncompressed bitmap data from a RLE4 stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// RLE4 is a 2-byte run-length encoding.
|
|
/// <br/>If first byte is 0, the second byte may have special meaning.
|
|
/// <br/>Otherwise, the first byte is the length of the run and second byte contains two color indexes.
|
|
/// </remarks>
|
|
/// <param name="w">The width of the bitmap.</param>
|
|
/// <param name="buffer">Buffer for uncompressed data.</param>
|
|
/// <param name="undefinedPixels">Keeps track over skipped and therefore undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
|
|
private void UncompressRle4(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
Span<byte> cmd = stackalloc byte[2];
|
|
int count = 0;
|
|
|
|
while (count < buffer.Length)
|
|
{
|
|
if (this.stream.Read(cmd, 0, cmd.Length) != 2)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap.");
|
|
}
|
|
|
|
if (cmd[0] == RleCommand)
|
|
{
|
|
switch (cmd[1])
|
|
{
|
|
case RleEndOfBitmap:
|
|
int skipEoB = buffer.Length - count;
|
|
RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
return;
|
|
|
|
case RleEndOfLine:
|
|
count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
case RleDelta:
|
|
int dx = this.stream.ReadByte();
|
|
int dy = this.stream.ReadByte();
|
|
count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
default:
|
|
// If the second byte > 2, we are in 'absolute mode'.
|
|
// The second byte contains the number of color indexes that follow.
|
|
int max = cmd[1];
|
|
int bytesToRead = (max + 1) / 2;
|
|
|
|
byte[] run = new byte[bytesToRead];
|
|
|
|
this.stream.Read(run, 0, run.Length);
|
|
|
|
int idx = 0;
|
|
for (int i = 0; i < max; i++)
|
|
{
|
|
byte twoPixels = run[idx];
|
|
if (i % 2 == 0)
|
|
{
|
|
buffer[count++] = (byte)((twoPixels >> 4) & 0xF);
|
|
}
|
|
else
|
|
{
|
|
buffer[count++] = (byte)(twoPixels & 0xF);
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
// Absolute mode data is aligned to two-byte word-boundary.
|
|
int padding = bytesToRead & 1;
|
|
|
|
this.stream.Skip(padding);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int max = cmd[0];
|
|
|
|
// The second byte contains two color indexes, one in its high-order 4 bits and one in its low-order 4 bits.
|
|
byte twoPixels = cmd[1];
|
|
byte rightPixel = (byte)(twoPixels & 0xF);
|
|
byte leftPixel = (byte)((twoPixels >> 4) & 0xF);
|
|
|
|
for (int idx = 0; idx < max; idx++)
|
|
{
|
|
if (idx % 2 == 0)
|
|
{
|
|
buffer[count] = leftPixel;
|
|
}
|
|
else
|
|
{
|
|
buffer[count] = rightPixel;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Produce uncompressed bitmap data from a RLE8 stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// RLE8 is a 2-byte run-length encoding.
|
|
/// <br/>If first byte is 0, the second byte may have special meaning.
|
|
/// <br/>Otherwise, the first byte is the length of the run and second byte is the color for the run.
|
|
/// </remarks>
|
|
/// <param name="w">The width of the bitmap.</param>
|
|
/// <param name="buffer">Buffer for uncompressed data.</param>
|
|
/// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
|
|
private void UncompressRle8(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
Span<byte> cmd = stackalloc byte[2];
|
|
int count = 0;
|
|
|
|
while (count < buffer.Length)
|
|
{
|
|
if (this.stream.Read(cmd, 0, cmd.Length) != 2)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap.");
|
|
}
|
|
|
|
if (cmd[0] == RleCommand)
|
|
{
|
|
switch (cmd[1])
|
|
{
|
|
case RleEndOfBitmap:
|
|
int skipEoB = buffer.Length - count;
|
|
RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
return;
|
|
|
|
case RleEndOfLine:
|
|
count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
case RleDelta:
|
|
int dx = this.stream.ReadByte();
|
|
int dy = this.stream.ReadByte();
|
|
count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
default:
|
|
// If the second byte > 2, we are in 'absolute mode'.
|
|
// Take this number of bytes from the stream as uncompressed data.
|
|
int length = cmd[1];
|
|
|
|
byte[] run = new byte[length];
|
|
|
|
this.stream.Read(run, 0, run.Length);
|
|
|
|
run.AsSpan().CopyTo(buffer[count..]);
|
|
|
|
count += run.Length;
|
|
|
|
// Absolute mode data is aligned to two-byte word-boundary.
|
|
int padding = length & 1;
|
|
|
|
this.stream.Skip(padding);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int max = count + cmd[0]; // as we start at the current count in the following loop, max is count + cmd[0]
|
|
byte colorIdx = cmd[1]; // store the value to avoid the repeated indexer access inside the loop.
|
|
|
|
for (; count < max; count++)
|
|
{
|
|
buffer[count] = colorIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Produce uncompressed bitmap data from a RLE24 stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <br/>If first byte is 0, the second byte may have special meaning.
|
|
/// <br/>Otherwise, the first byte is the length of the run and following three bytes are the color for the run.
|
|
/// </remarks>
|
|
/// <param name="w">The width of the bitmap.</param>
|
|
/// <param name="buffer">Buffer for uncompressed data.</param>
|
|
/// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
|
|
private void UncompressRle24(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
Span<byte> cmd = stackalloc byte[2];
|
|
int uncompressedPixels = 0;
|
|
|
|
while (uncompressedPixels < buffer.Length)
|
|
{
|
|
if (this.stream.Read(cmd, 0, cmd.Length) != 2)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap.");
|
|
}
|
|
|
|
if (cmd[0] == RleCommand)
|
|
{
|
|
switch (cmd[1])
|
|
{
|
|
case RleEndOfBitmap:
|
|
int skipEoB = (buffer.Length - (uncompressedPixels * 3)) / 3;
|
|
RleSkipEndOfBitmap(uncompressedPixels, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
return;
|
|
|
|
case RleEndOfLine:
|
|
uncompressedPixels += RleSkipEndOfLine(uncompressedPixels, w, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
case RleDelta:
|
|
int dx = this.stream.ReadByte();
|
|
int dy = this.stream.ReadByte();
|
|
uncompressedPixels += RleSkipDelta(uncompressedPixels, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
|
|
|
|
break;
|
|
|
|
default:
|
|
// If the second byte > 2, we are in 'absolute mode'.
|
|
// Take this number of bytes from the stream as uncompressed data.
|
|
int length = cmd[1];
|
|
|
|
byte[] run = new byte[length * 3];
|
|
|
|
this.stream.Read(run, 0, run.Length);
|
|
|
|
run.AsSpan().CopyTo(buffer[(uncompressedPixels * 3)..]);
|
|
|
|
uncompressedPixels += length;
|
|
|
|
// Absolute mode data is aligned to two-byte word-boundary.
|
|
int padding = run.Length & 1;
|
|
|
|
this.stream.Skip(padding);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int max = uncompressedPixels + cmd[0];
|
|
byte blueIdx = cmd[1];
|
|
byte greenIdx = (byte)this.stream.ReadByte();
|
|
byte redIdx = (byte)this.stream.ReadByte();
|
|
|
|
int bufferIdx = uncompressedPixels * 3;
|
|
for (; uncompressedPixels < max; uncompressedPixels++)
|
|
{
|
|
buffer[bufferIdx++] = blueIdx;
|
|
buffer[bufferIdx++] = greenIdx;
|
|
buffer[bufferIdx++] = redIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps track of skipped / undefined pixels, when the EndOfBitmap command occurs.
|
|
/// </summary>
|
|
/// <param name="count">The already processed pixel count.</param>
|
|
/// <param name="w">The width of the image.</param>
|
|
/// <param name="skipPixelCount">The skipped pixel count.</param>
|
|
/// <param name="undefinedPixels">The undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">Rows with undefined pixels.</param>
|
|
private static void RleSkipEndOfBitmap(
|
|
int count,
|
|
int w,
|
|
int skipPixelCount,
|
|
Span<bool> undefinedPixels,
|
|
Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
for (int i = count; i < count + skipPixelCount; i++)
|
|
{
|
|
undefinedPixels[i] = true;
|
|
}
|
|
|
|
int skippedRowIdx = count / w;
|
|
int skippedRows = (skipPixelCount / w) - 1;
|
|
int lastSkippedRow = Math.Min(skippedRowIdx + skippedRows, rowsWithUndefinedPixels.Length - 1);
|
|
for (int i = skippedRowIdx; i <= lastSkippedRow; i++)
|
|
{
|
|
rowsWithUndefinedPixels[i] = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps track of undefined / skipped pixels, when the EndOfLine command occurs.
|
|
/// </summary>
|
|
/// <param name="count">The already uncompressed pixel count.</param>
|
|
/// <param name="w">The width of image.</param>
|
|
/// <param name="undefinedPixels">The undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">The rows with undefined pixels.</param>
|
|
/// <returns>The number of skipped pixels.</returns>
|
|
private static int RleSkipEndOfLine(int count, int w, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
rowsWithUndefinedPixels[count / w] = true;
|
|
int remainingPixelsInRow = count % w;
|
|
if (remainingPixelsInRow > 0)
|
|
{
|
|
int skipEoL = w - remainingPixelsInRow;
|
|
for (int i = count; i < count + skipEoL; i++)
|
|
{
|
|
undefinedPixels[i] = true;
|
|
}
|
|
|
|
return skipEoL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps track of undefined / skipped pixels, when the delta command occurs.
|
|
/// </summary>
|
|
/// <param name="count">The count.</param>
|
|
/// <param name="w">The width of the image.</param>
|
|
/// <param name="dx">Delta skip in x direction.</param>
|
|
/// <param name="dy">Delta skip in y direction.</param>
|
|
/// <param name="undefinedPixels">The undefined pixels.</param>
|
|
/// <param name="rowsWithUndefinedPixels">The rows with undefined pixels.</param>
|
|
/// <returns>The number of skipped pixels.</returns>
|
|
private static int RleSkipDelta(
|
|
int count,
|
|
int w,
|
|
int dx,
|
|
int dy,
|
|
Span<bool> undefinedPixels,
|
|
Span<bool> rowsWithUndefinedPixels)
|
|
{
|
|
int skipDelta = (w * dy) + dx;
|
|
for (int i = count; i < count + skipDelta; i++)
|
|
{
|
|
undefinedPixels[i] = true;
|
|
}
|
|
|
|
int skippedRowIdx = count / w;
|
|
int lastSkippedRow = Math.Min(skippedRowIdx + dy, rowsWithUndefinedPixels.Length - 1);
|
|
for (int i = skippedRowIdx; i <= lastSkippedRow; i++)
|
|
{
|
|
rowsWithUndefinedPixels[i] = true;
|
|
}
|
|
|
|
return skipDelta;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the color palette from the stream.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
|
|
/// <param name="bytesPerColorMapEntry">Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
|
|
/// the bytes per color palette entry's can be 3 bytes instead of 4.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRgbPalette<TPixel>(Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
// Pixels per byte (bits per pixel).
|
|
int ppb = 8 / bitsPerPixel;
|
|
|
|
int arrayWidth = (width + ppb - 1) / ppb;
|
|
|
|
// Bit mask
|
|
int mask = 0xFF >> (8 - bitsPerPixel);
|
|
|
|
// Rows are aligned on 4 byte boundaries.
|
|
int padding = arrayWidth % 4;
|
|
if (padding != 0)
|
|
{
|
|
padding = 4 - padding;
|
|
}
|
|
|
|
using IMemoryOwner<byte> row = this.memoryAllocator.Allocate<byte>(arrayWidth + padding, AllocationOptions.Clean);
|
|
TPixel color = default;
|
|
Span<byte> rowSpan = row.GetSpan();
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
int newY = Invert(y, height, inverted);
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int offset = 0;
|
|
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
|
|
|
|
for (int x = 0; x < arrayWidth; x++)
|
|
{
|
|
int colOffset = x * ppb;
|
|
for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++)
|
|
{
|
|
int colorIndex = ((rowSpan[offset] >> (8 - bitsPerPixel - (shift * bitsPerPixel))) & mask) * bytesPerColorMapEntry;
|
|
|
|
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIndex]));
|
|
pixelRow[newX] = color;
|
|
}
|
|
|
|
offset++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the 16 bit color palette from the stream.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
/// <param name="redMask">The bitmask for the red channel.</param>
|
|
/// <param name="greenMask">The bitmask for the green channel.</param>
|
|
/// <param name="blueMask">The bitmask for the blue channel.</param>
|
|
private void ReadRgb16<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
int padding = CalculatePadding(width, 2);
|
|
int stride = (width * 2) + padding;
|
|
TPixel color = default;
|
|
|
|
int rightShiftRedMask = CalculateRightShift((uint)redMask);
|
|
int rightShiftGreenMask = CalculateRightShift((uint)greenMask);
|
|
int rightShiftBlueMask = CalculateRightShift((uint)blueMask);
|
|
|
|
// Each color channel contains either 5 or 6 Bits values.
|
|
int redMaskBits = CountBits((uint)redMask);
|
|
int greenMaskBits = CountBits((uint)greenMask);
|
|
int blueMaskBits = CountBits((uint)blueMask);
|
|
|
|
using IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(stride);
|
|
Span<byte> bufferSpan = buffer.GetSpan();
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(bufferSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
|
|
|
|
int offset = 0;
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
short temp = BinaryPrimitives.ReadInt16LittleEndian(bufferSpan[offset..]);
|
|
|
|
// Rescale values, so the values range from 0 to 255.
|
|
int r = (redMaskBits == 5) ? GetBytesFrom5BitValue((temp & redMask) >> rightShiftRedMask) : GetBytesFrom6BitValue((temp & redMask) >> rightShiftRedMask);
|
|
int g = (greenMaskBits == 5) ? GetBytesFrom5BitValue((temp & greenMask) >> rightShiftGreenMask) : GetBytesFrom6BitValue((temp & greenMask) >> rightShiftGreenMask);
|
|
int b = (blueMaskBits == 5) ? GetBytesFrom5BitValue((temp & blueMask) >> rightShiftBlueMask) : GetBytesFrom6BitValue((temp & blueMask) >> rightShiftBlueMask);
|
|
Rgb24 rgb = new((byte)r, (byte)g, (byte)b);
|
|
|
|
color.FromRgb24(rgb);
|
|
pixelRow[x] = color;
|
|
offset += 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs final shifting from a 5bit value to an 8bit one.
|
|
/// </summary>
|
|
/// <param name="value">The masked and shifted value.</param>
|
|
/// <returns>The <see cref="byte"/></returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
|
|
|
|
/// <summary>
|
|
/// Performs final shifting from a 6bit value to an 8bit one.
|
|
/// </summary>
|
|
/// <param name="value">The masked and shifted value.</param>
|
|
/// <returns>The <see cref="byte"/></returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4));
|
|
|
|
/// <summary>
|
|
/// Reads the 24 bit color palette from the stream.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRgb24<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
int padding = CalculatePadding(width, 3);
|
|
using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 3, padding);
|
|
Span<byte> rowSpan = row.GetSpan();
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
|
|
PixelOperations<TPixel>.Instance.FromBgr24Bytes(
|
|
this.configuration,
|
|
rowSpan,
|
|
pixelSpan,
|
|
width);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the 32 bit color palette from the stream.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRgb32Fast<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
int padding = CalculatePadding(width, 4);
|
|
using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding);
|
|
Span<byte> rowSpan = row.GetSpan();
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
|
|
PixelOperations<TPixel>.Instance.FromBgra32Bytes(
|
|
this.configuration,
|
|
rowSpan,
|
|
pixelSpan,
|
|
width);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the 32 bit color palette from the stream, checking the alpha component of each pixel.
|
|
/// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
|
|
/// <param name="width">The width of the bitmap.</param>
|
|
/// <param name="height">The height of the bitmap.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
private void ReadRgb32Slow<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
int padding = CalculatePadding(width, 4);
|
|
using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding);
|
|
using IMemoryOwner<Bgra32> bgraRow = this.memoryAllocator.Allocate<Bgra32>(width);
|
|
Span<byte> rowSpan = row.GetSpan();
|
|
Span<Bgra32> bgraRowSpan = bgraRow.GetSpan();
|
|
long currentPosition = this.stream.Position;
|
|
bool hasAlpha = false;
|
|
|
|
// Loop though the rows checking each pixel. We start by assuming it's
|
|
// an BGR0 image. If we hit a non-zero alpha value, then we know it's
|
|
// actually a BGRA image, and change tactics accordingly.
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
PixelOperations<Bgra32>.Instance.FromBgra32Bytes(
|
|
this.configuration,
|
|
rowSpan,
|
|
bgraRowSpan,
|
|
width);
|
|
|
|
// Check each pixel in the row to see if it has an alpha value.
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
Bgra32 bgra = bgraRowSpan[x];
|
|
if (bgra.A > 0)
|
|
{
|
|
hasAlpha = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasAlpha)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Reset our stream for a second pass.
|
|
this.stream.Position = currentPosition;
|
|
|
|
// Process the pixels in bulk taking the raw alpha component value.
|
|
if (hasAlpha)
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
|
|
|
|
PixelOperations<TPixel>.Instance.FromBgra32Bytes(
|
|
this.configuration,
|
|
rowSpan,
|
|
pixelSpan,
|
|
width);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Slow path. We need to set each alpha component value to fully opaque.
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(rowSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
PixelOperations<Bgra32>.Instance.FromBgra32Bytes(
|
|
this.configuration,
|
|
rowSpan,
|
|
bgraRowSpan,
|
|
width);
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
|
|
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
Bgra32 bgra = bgraRowSpan[x];
|
|
bgra.A = byte.MaxValue;
|
|
ref TPixel pixel = ref pixelSpan[x];
|
|
pixel.FromBgra32(bgra);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode an 32 Bit Bitmap containing a bitmask for each color channel.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
/// <param name="pixels">The output pixel buffer containing the decoded image.</param>
|
|
/// <param name="width">The width of the image.</param>
|
|
/// <param name="height">The height of the image.</param>
|
|
/// <param name="inverted">Whether the bitmap is inverted.</param>
|
|
/// <param name="redMask">The bitmask for the red channel.</param>
|
|
/// <param name="greenMask">The bitmask for the green channel.</param>
|
|
/// <param name="blueMask">The bitmask for the blue channel.</param>
|
|
/// <param name="alphaMask">The bitmask for the alpha channel.</param>
|
|
private void ReadRgb32BitFields<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
TPixel color = default;
|
|
int padding = CalculatePadding(width, 4);
|
|
int stride = (width * 4) + padding;
|
|
|
|
int rightShiftRedMask = CalculateRightShift((uint)redMask);
|
|
int rightShiftGreenMask = CalculateRightShift((uint)greenMask);
|
|
int rightShiftBlueMask = CalculateRightShift((uint)blueMask);
|
|
int rightShiftAlphaMask = CalculateRightShift((uint)alphaMask);
|
|
|
|
int bitsRedMask = CountBits((uint)redMask);
|
|
int bitsGreenMask = CountBits((uint)greenMask);
|
|
int bitsBlueMask = CountBits((uint)blueMask);
|
|
int bitsAlphaMask = CountBits((uint)alphaMask);
|
|
float invMaxValueRed = 1.0f / (0xFFFFFFFF >> (32 - bitsRedMask));
|
|
float invMaxValueGreen = 1.0f / (0xFFFFFFFF >> (32 - bitsGreenMask));
|
|
float invMaxValueBlue = 1.0f / (0xFFFFFFFF >> (32 - bitsBlueMask));
|
|
uint maxValueAlpha = 0xFFFFFFFF >> (32 - bitsAlphaMask);
|
|
float invMaxValueAlpha = 1.0f / maxValueAlpha;
|
|
|
|
bool unusualBitMask = bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8;
|
|
|
|
using IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(stride);
|
|
Span<byte> bufferSpan = buffer.GetSpan();
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
if (this.stream.Read(bufferSpan) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
|
|
}
|
|
|
|
int newY = Invert(y, height, inverted);
|
|
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
|
|
|
|
int offset = 0;
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
uint temp = BinaryPrimitives.ReadUInt32LittleEndian(bufferSpan[offset..]);
|
|
|
|
if (unusualBitMask)
|
|
{
|
|
uint r = (uint)(temp & redMask) >> rightShiftRedMask;
|
|
uint g = (uint)(temp & greenMask) >> rightShiftGreenMask;
|
|
uint b = (uint)(temp & blueMask) >> rightShiftBlueMask;
|
|
float alpha = alphaMask != 0 ? invMaxValueAlpha * ((uint)(temp & alphaMask) >> rightShiftAlphaMask) : 1.0f;
|
|
Vector4 vector4 = new(
|
|
r * invMaxValueRed,
|
|
g * invMaxValueGreen,
|
|
b * invMaxValueBlue,
|
|
alpha);
|
|
color.FromScaledVector4(vector4);
|
|
}
|
|
else
|
|
{
|
|
byte r = (byte)((temp & redMask) >> rightShiftRedMask);
|
|
byte g = (byte)((temp & greenMask) >> rightShiftGreenMask);
|
|
byte b = (byte)((temp & blueMask) >> rightShiftBlueMask);
|
|
byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : (byte)255;
|
|
color.FromRgba32(new Rgba32(r, g, b, a));
|
|
}
|
|
|
|
pixelRow[x] = color;
|
|
offset += 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the necessary right shifts for a given color bitmask (the 0 bits to the right).
|
|
/// </summary>
|
|
/// <param name="n">The color bit mask.</param>
|
|
/// <returns>Number of bits to shift right.</returns>
|
|
private static int CalculateRightShift(uint n)
|
|
{
|
|
int count = 0;
|
|
while (n > 0)
|
|
{
|
|
if ((1 & n) == 0)
|
|
{
|
|
count++;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
n >>= 1;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts none zero bits.
|
|
/// </summary>
|
|
/// <param name="n">A color mask.</param>
|
|
/// <returns>The none zero bits.</returns>
|
|
private static int CountBits(uint n)
|
|
{
|
|
int count = 0;
|
|
while (n != 0)
|
|
{
|
|
count++;
|
|
n &= n - 1;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the <see cref="BmpInfoHeader"/> from the stream.
|
|
/// </summary>
|
|
private void ReadInfoHeader()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize];
|
|
long infoHeaderStart = this.stream.Position;
|
|
|
|
// Resolution is stored in PPM.
|
|
this.metadata = new ImageMetadata
|
|
{
|
|
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
|
|
};
|
|
|
|
// Read the header size.
|
|
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
|
|
|
|
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
|
|
if (headerSize is < BmpInfoHeader.CoreSize or > BmpInfoHeader.MaxHeaderSize)
|
|
{
|
|
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'.");
|
|
}
|
|
|
|
// Read the rest of the header.
|
|
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
|
|
|
|
BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2;
|
|
if (headerSize == BmpInfoHeader.CoreSize)
|
|
{
|
|
// 12 bytes
|
|
infoHeaderType = BmpInfoHeaderType.WinVersion2;
|
|
this.infoHeader = BmpInfoHeader.ParseCore(buffer);
|
|
}
|
|
else if (headerSize == BmpInfoHeader.Os22ShortSize)
|
|
{
|
|
// 16 bytes
|
|
infoHeaderType = BmpInfoHeaderType.Os2Version2Short;
|
|
this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer);
|
|
}
|
|
else if (headerSize == BmpInfoHeader.SizeV3)
|
|
{
|
|
// == 40 bytes
|
|
infoHeaderType = BmpInfoHeaderType.WinVersion3;
|
|
this.infoHeader = BmpInfoHeader.ParseV3(buffer);
|
|
|
|
// If the info header is BMP version 3 and the compression type is BITFIELDS,
|
|
// color masks for each color channel follow the info header.
|
|
if (this.infoHeader.Compression == BmpCompression.BitFields)
|
|
{
|
|
byte[] bitfieldsBuffer = new byte[12];
|
|
this.stream.Read(bitfieldsBuffer, 0, 12);
|
|
Span<byte> data = bitfieldsBuffer.AsSpan();
|
|
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
|
|
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
|
|
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
|
|
}
|
|
else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS)
|
|
{
|
|
byte[] bitfieldsBuffer = new byte[16];
|
|
this.stream.Read(bitfieldsBuffer, 0, 16);
|
|
Span<byte> data = bitfieldsBuffer.AsSpan();
|
|
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
|
|
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
|
|
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
|
|
this.infoHeader.AlphaMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(12, 4));
|
|
}
|
|
}
|
|
else if (headerSize == BmpInfoHeader.AdobeV3Size)
|
|
{
|
|
// == 52 bytes
|
|
infoHeaderType = BmpInfoHeaderType.AdobeVersion3;
|
|
this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false);
|
|
}
|
|
else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize)
|
|
{
|
|
// == 56 bytes
|
|
infoHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha;
|
|
this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true);
|
|
}
|
|
else if (headerSize == BmpInfoHeader.Os2v2Size)
|
|
{
|
|
// == 64 bytes
|
|
infoHeaderType = BmpInfoHeaderType.Os2Version2;
|
|
this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer);
|
|
}
|
|
else if (headerSize == BmpInfoHeader.SizeV4)
|
|
{
|
|
// == 108 bytes
|
|
infoHeaderType = BmpInfoHeaderType.WinVersion4;
|
|
this.infoHeader = BmpInfoHeader.ParseV4(buffer);
|
|
}
|
|
else if (headerSize > BmpInfoHeader.SizeV4)
|
|
{
|
|
// > 108 bytes
|
|
infoHeaderType = BmpInfoHeaderType.WinVersion5;
|
|
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
|
|
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
|
|
{
|
|
// Read color profile.
|
|
long streamPosition = this.stream.Position;
|
|
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
|
|
this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
|
|
this.stream.Read(iccProfileData);
|
|
this.metadata.IccProfile = new IccProfile(iccProfileData);
|
|
this.stream.Position = streamPosition;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'.");
|
|
}
|
|
|
|
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
|
|
{
|
|
this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter;
|
|
this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter;
|
|
}
|
|
else
|
|
{
|
|
// Convert default metadata values to PPM.
|
|
this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution));
|
|
this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
|
|
}
|
|
|
|
short bitsPerPixel = this.infoHeader.BitsPerPixel;
|
|
this.bmpMetadata = this.metadata.GetBmpMetadata();
|
|
this.bmpMetadata.InfoHeaderType = infoHeaderType;
|
|
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the <see cref="BmpFileHeader"/> from the stream.
|
|
/// </summary>
|
|
private void ReadFileHeader()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[BmpFileHeader.Size];
|
|
this.stream.Read(buffer, 0, BmpFileHeader.Size);
|
|
|
|
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer);
|
|
switch (fileTypeMarker)
|
|
{
|
|
case BmpConstants.TypeMarkers.Bitmap:
|
|
this.fileMarkerType = BmpFileMarkerType.Bitmap;
|
|
this.fileHeader = BmpFileHeader.Parse(buffer);
|
|
break;
|
|
case BmpConstants.TypeMarkers.BitmapArray:
|
|
this.fileMarkerType = BmpFileMarkerType.BitmapArray;
|
|
|
|
// Because we only decode the first bitmap in the array, the array header will be ignored.
|
|
// The bitmap file header of the first image follows the array header.
|
|
this.stream.Read(buffer, 0, BmpFileHeader.Size);
|
|
this.fileHeader = BmpFileHeader.Parse(buffer);
|
|
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
|
|
{
|
|
BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'.");
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{fileTypeMarker}'.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the <see cref="BmpFileHeader"/> and <see cref="BmpInfoHeader"/> from the stream and sets the corresponding fields.
|
|
/// </summary>
|
|
/// <param name="stream">The input stream.</param>
|
|
/// <param name="inverted">Whether the image orientation is inverted.</param>
|
|
/// <param name="palette">The color palette.</param>
|
|
/// <returns>Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
|
|
/// the bytes per color palette entry's can be 3 bytes instead of 4.</returns>
|
|
private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette)
|
|
{
|
|
this.stream = stream;
|
|
|
|
this.ReadFileHeader();
|
|
this.ReadInfoHeader();
|
|
|
|
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
|
|
// If the height is negative, then this is a Windows bitmap whose origin
|
|
// is the upper-left corner and not the lower-left. The inverted flag
|
|
// indicates a lower-left origin.Our code will be outputting an
|
|
// upper-left origin pixel array.
|
|
inverted = false;
|
|
if (this.infoHeader.Height < 0)
|
|
{
|
|
inverted = true;
|
|
this.infoHeader.Height = -this.infoHeader.Height;
|
|
}
|
|
|
|
int bytesPerColorMapEntry = 4;
|
|
int colorMapSizeBytes = -1;
|
|
if (this.infoHeader.ClrUsed == 0)
|
|
{
|
|
if (this.infoHeader.BitsPerPixel is 1 or 2 or 4 or 8)
|
|
{
|
|
switch (this.fileMarkerType)
|
|
{
|
|
case BmpFileMarkerType.Bitmap:
|
|
colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
|
|
int colorCountForBitDepth = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
|
|
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
|
|
|
|
// Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3.
|
|
bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3);
|
|
|
|
break;
|
|
case BmpFileMarkerType.BitmapArray:
|
|
case BmpFileMarkerType.ColorIcon:
|
|
case BmpFileMarkerType.ColorPointer:
|
|
case BmpFileMarkerType.Icon:
|
|
case BmpFileMarkerType.Pointer:
|
|
// OS/2 bitmaps always have 3 colors per color palette entry.
|
|
bytesPerColorMapEntry = 3;
|
|
colorMapSizeBytes = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * bytesPerColorMapEntry;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
|
|
}
|
|
|
|
palette = null;
|
|
|
|
if (colorMapSizeBytes > 0)
|
|
{
|
|
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit.
|
|
// Make sure, that we will not read pass the bitmap offset (starting position of image data).
|
|
if ((this.stream.Position + colorMapSizeBytes) > this.fileHeader.Offset)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException(
|
|
$"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");
|
|
}
|
|
|
|
palette = new byte[colorMapSizeBytes];
|
|
|
|
if (this.stream.Read(palette, 0, colorMapSizeBytes) == 0)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the palette!");
|
|
}
|
|
}
|
|
|
|
int skipAmount = this.fileHeader.Offset - (int)this.stream.Position;
|
|
if ((skipAmount + (int)this.stream.Position) > this.stream.Length)
|
|
{
|
|
BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length.");
|
|
}
|
|
|
|
if (skipAmount > 0)
|
|
{
|
|
this.stream.Skip(skipAmount);
|
|
}
|
|
|
|
return bytesPerColorMapEntry;
|
|
}
|
|
}
|
|
|