From f62e2baac8957b73f0b910d61549087b40f0761a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Thu, 14 Dec 2023 14:13:00 +0800 Subject: [PATCH] Make the BMP Decoder Core can skip the file header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make BmpDecoderCore support AlphaMask. link #687 Signed-off-by: 舰队的偶像-岛风酱! --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 226 +++++++++++++++--- .../Formats/Bmp/BmpDecoderOptions.cs | 24 ++ 2 files changed, 211 insertions(+), 39 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index bed489752..9bea6867a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -71,7 +71,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// /// The file header containing general information. /// - private BmpFileHeader fileHeader; + private BmpFileHeader? fileHeader; /// /// Indicates which bitmap file marker was read. @@ -99,6 +99,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// private readonly RleSkippedPixelHandling rleSkippedPixelHandling; + /// + private readonly bool processedAlphaMask; + + /// + private readonly bool skipFileHeader; + + /// + private readonly bool isDoubleHeight; + /// /// Initializes a new instance of the class. /// @@ -109,6 +118,9 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals this.rleSkippedPixelHandling = options.RleSkippedPixelHandling; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; + this.processedAlphaMask = options.ProcessedAlphaMask; + this.skipFileHeader = options.SkipFileHeader; + this.isDoubleHeight = options.IsDoubleHeight; } /// @@ -132,38 +144,44 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals switch (this.infoHeader.Compression) { - case BmpCompression.RGB: - if (this.infoHeader.BitsPerPixel == 32) - { - if (this.bmpMetadata.InfoHeaderType == BmpInfoHeaderType.WinVersion3) - { - this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); - } - else - { - this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); - } - } - else if (this.infoHeader.BitsPerPixel == 24) - { - this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); - } - else if (this.infoHeader.BitsPerPixel == 16) - { - this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); - } - else if (this.infoHeader.BitsPerPixel <= 8) - { - this.ReadRgbPalette( - stream, - pixels, - palette, - this.infoHeader.Width, - this.infoHeader.Height, - this.infoHeader.BitsPerPixel, - bytesPerColorMapEntry, - inverted); - } + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: + this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32: + this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24: + this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16: + this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask: + this.ReadRgbPaletteWithAlphaMask( + stream, + pixels, + palette, + this.infoHeader.Width, + this.infoHeader.Height, + this.infoHeader.BitsPerPixel, + bytesPerColorMapEntry, + inverted); + + break; + case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8: + this.ReadRgbPalette( + stream, + pixels, + palette, + this.infoHeader.Width, + this.infoHeader.Height, + this.infoHeader.BitsPerPixel, + bytesPerColorMapEntry, + inverted); break; @@ -839,6 +857,108 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals } } + /// + private void ReadRgbPaletteWithAlphaMask(BufferedReadStream stream, Buffer2D pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted) + where TPixel : unmanaged, IPixel + { + // 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; + } + + Bgra32[,] image = new Bgra32[height, width]; + using (IMemoryOwner row = this.memoryAllocator.Allocate(arrayWidth + padding, AllocationOptions.Clean)) + { + Span rowSpan = row.GetSpan(); + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + if (stream.Read(rowSpan) == 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); + } + + int offset = 0; + + 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; + + image[newY, newX].FromBgr24(Unsafe.As(ref colors[colorIndex])); + } + + offset++; + } + } + } + + arrayWidth = width / 8; + padding = arrayWidth % 4; + if (padding != 0) + { + padding = 4 - padding; + } + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + + for (int i = 0; i < arrayWidth; i++) + { + int x = i * 8; + int and = stream.ReadByte(); + if (and is -1) + { + throw new EndOfStreamException(); + } + + for (int j = 0; j < 8; j++) + { + SetAlpha(ref image[newY, x + j], and, j); + } + } + + stream.Skip(padding); + } + + for (int y = 0; y < height; y++) + { + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.DangerousGetRowSpan(newY); + + for (int x = 0; x < width; x++) + { + pixelRow[x].FromBgra32(image[newY, x]); + } + } + } + + /// + /// Set pixel's alpha with alpha mask. + /// + /// Bgra32 pixel. + /// alpha mask. + /// bit index of pixel. + private static void SetAlpha(ref Bgra32 pixel, in int mask, in int index) + { + bool isTransparently = (mask & (0b10000000 >> index)) is not 0; + pixel.A = isTransparently ? byte.MinValue : byte.MaxValue; + } + /// /// Reads the 16 bit color palette from the stream. /// @@ -1333,6 +1453,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); } + if (this.isDoubleHeight) + { + this.infoHeader.Height >>= 1; + } + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; @@ -1362,9 +1487,9 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals // The bitmap file header of the first image follows the array header. stream.Read(buffer, 0, BmpFileHeader.Size); this.fileHeader = BmpFileHeader.Parse(buffer); - if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap) + if (this.fileHeader.Value.Type != BmpConstants.TypeMarkers.Bitmap) { - BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'."); + BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Value.Type}'."); } break; @@ -1387,7 +1512,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals [MemberNotNull(nameof(bmpMetadata))] private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette) { - this.ReadFileHeader(stream); + if (!this.skipFileHeader) + { + this.ReadFileHeader(stream); + } + this.ReadInfoHeader(stream); // see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517 @@ -1411,7 +1540,21 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals switch (this.fileMarkerType) { case BmpFileMarkerType.Bitmap: - colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize; + if (this.fileHeader.HasValue) + { + colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize; + } + else + { + colorMapSizeBytes = this.infoHeader.ClrUsed; + if (colorMapSizeBytes is 0 && this.infoHeader.BitsPerPixel is <= 8) + { + colorMapSizeBytes = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel); + } + + colorMapSizeBytes *= 4; + } + int colorCountForBitDepth = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel); bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth; @@ -1442,7 +1585,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals { // 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 (stream.Position > this.fileHeader.Offset - colorMapSizeBytes) + if (this.fileHeader.HasValue && stream.Position > this.fileHeader.Value.Offset - colorMapSizeBytes) { 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."); @@ -1456,7 +1599,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals } } - int skipAmount = this.fileHeader.Offset - (int)stream.Position; + int skipAmount = 0; + if (this.fileHeader.HasValue) + { + skipAmount = this.fileHeader.Value.Offset - (int)stream.Position; + } + if ((skipAmount + (int)stream.Position) > stream.Length) { BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length."); diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs index b3387ce80..17d37cab2 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs @@ -16,4 +16,28 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions /// which can occur during decoding run length encoded bitmaps. /// public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; } + + /// + /// Gets a value indicating whether the additional AlphaMask is processed at decoding time. + /// + /// + /// It will be used at IcoDecoder. + /// + internal bool ProcessedAlphaMask { get; init; } + + /// + /// Gets a value indicating whether to skip loading the BMP file header. + /// + /// + /// It will be used at IcoDecoder. + /// + internal bool SkipFileHeader { get; init; } + + /// + /// Gets a value indicating whether the height is double of true height. + /// + /// + /// It will be used at IcoDecoder. + /// + internal bool IsDoubleHeight { get; init; } }