Skip to content

Commit

Permalink
Merge pull request #2579 from frg2089/feat/icon
Browse files Browse the repository at this point in the history
Add ICO and CUR file decoder.
  • Loading branch information
JimBobSquarePants authored Jun 19, 2024
2 parents 339b26d + b31bd23 commit 3b49c34
Show file tree
Hide file tree
Showing 162 changed files with 2,722 additions and 58 deletions.
7 changes: 7 additions & 0 deletions ImageSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-493
tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{95E45DDE-A67D-48AD-BBA8-5FAA151B860D}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Icon\aero_arrow.cur = tests\Images\Input\Icon\aero_arrow.cur
tests\Images\Input\Icon\flutter.ico = tests\Images\Input\Icon\flutter.ico
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -714,6 +720,7 @@ Global
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{95E45DDE-A67D-48AD-BBA8-5FAA151B860D} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
Expand Down
6 changes: 5 additions & 1 deletion src/ImageSharp/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Collections.Concurrent;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
Expand Down Expand Up @@ -222,5 +224,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
new QoiConfigurationModule());
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
}
7 changes: 6 additions & 1 deletion src/ImageSharp/Formats/Bmp/BmpConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ internal static class BmpConstants
/// <summary>
/// The list of mimetypes that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/bmp", "image/x-windows-bmp" };
public static readonly IEnumerable<string> MimeTypes = new[]
{
"image/bmp",
"image/x-windows-bmp",
"image/x-win-bitmap"
};

/// <summary>
/// The list of file extensions that equate to a bmp.
Expand Down
235 changes: 196 additions & 39 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
Expand Down Expand Up @@ -71,7 +72,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <summary>
/// The file header containing general information.
/// </summary>
private BmpFileHeader fileHeader;
private BmpFileHeader? fileHeader;

/// <summary>
/// Indicates which bitmap file marker was read.
Expand Down Expand Up @@ -99,6 +100,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// </summary>
private readonly RleSkippedPixelHandling rleSkippedPixelHandling;

/// <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="BmpDecoderCore"/> class.
/// </summary>
Expand All @@ -109,6 +119,9 @@ public BmpDecoderCore(BmpDecoderOptions options)
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.UseDoubleHeight;
}

/// <inheritdoc />
Expand All @@ -132,38 +145,44 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken

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;

Expand Down Expand Up @@ -839,6 +858,108 @@ private void ReadRgbPalette<TPixel>(BufferedReadStream stream, Buffer2D<TPixel>
}
}

/// <inheritdoc cref="ReadRgbPalette"/>
private void ReadRgbPaletteWithAlphaMask<TPixel>(BufferedReadStream stream, 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;
}

Bgra32[,] image = new Bgra32[height, width];
using (IMemoryOwner<byte> row = this.memoryAllocator.Allocate<byte>(arrayWidth + padding, AllocationOptions.Clean))
{
Span<byte> 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] = Bgra32.FromBgr24(Unsafe.As<byte, Bgr24>(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<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);

for (int x = 0; x < width; x++)
{
pixelRow[x] = TPixel.FromBgra32(image[newY, x]);
}
}
}

/// <summary>
/// Set pixel's alpha with alpha mask.
/// </summary>
/// <param name="pixel">Bgra32 pixel.</param>
/// <param name="mask">alpha mask.</param>
/// <param name="index">bit index of pixel.</param>
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;
}

/// <summary>
/// Reads the 16 bit color palette from the stream.
/// </summary>
Expand Down Expand Up @@ -1333,6 +1454,11 @@ private void ReadInfoHeader(BufferedReadStream stream)
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;
Expand Down Expand Up @@ -1362,9 +1488,9 @@ private void ReadFileHeader(BufferedReadStream stream)
// 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;
Expand All @@ -1387,7 +1513,11 @@ private void ReadFileHeader(BufferedReadStream stream)
[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
Expand All @@ -1411,7 +1541,21 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
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;

Expand Down Expand Up @@ -1442,7 +1586,7 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
{
// 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.");
Expand All @@ -1456,7 +1600,20 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
}
}

int skipAmount = this.fileHeader.Offset - (int)stream.Position;
if (palette.Length > 0)
{
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Bgr24>()];
ReadOnlySpan<Bgr24> rgbTable = MemoryMarshal.Cast<byte, Bgr24>(palette);
Color.FromPixel(rgbTable, colorTable);
this.bmpMetadata.ColorTable = colorTable;
}

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.");
Expand Down
Loading

0 comments on commit 3b49c34

Please sign in to comment.