Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ICO and CUR file decoder. #2579

Merged
merged 29 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f62e2ba
Make the BMP Decoder Core can skip the file header
frg2089 Dec 14, 2023
520ff46
Append BMP's MIME Type
frg2089 Dec 14, 2023
1129382
Add UnsafeSetFormatMetadata in ImageFrameMetadata
frg2089 Dec 14, 2023
4832d8f
Add Icon Support
frg2089 Dec 14, 2023
ffde9a9
Refactor decoder and add notes
JimBobSquarePants Dec 14, 2023
79f5387
Handle frames exceeding 256 pixels
JimBobSquarePants Dec 14, 2023
a811784
Update TgaFileHeaderTests.cs
JimBobSquarePants Dec 14, 2023
43ea25f
Add Parse Method and Check Stream
frg2089 Dec 14, 2023
05d33f2
Remove IconFrameMetadata
frg2089 Dec 14, 2023
729d64e
Refactor SetFrameMetadata
frg2089 Dec 14, 2023
63da967
Flatten namespace.
frg2089 Dec 14, 2023
794b2a6
Refactor IconImageFormatDetector.
frg2089 Dec 14, 2023
746e742
Fixed errors and warnings
frg2089 Dec 14, 2023
dbd1783
Add encoders that cannot be used.
frg2089 Dec 14, 2023
54e87ab
Remove a comment
frg2089 Dec 14, 2023
21b1e71
Encoder Worked!
frg2089 Dec 14, 2023
e252a6f
Using Seek requires consideration of whether this stream allows Seek …
frg2089 Dec 14, 2023
fae15ae
Fixed AlphaMask
frg2089 Dec 14, 2023
52f392d
Fixed some warnings.
frg2089 Dec 14, 2023
5be5d64
fix
Poker-sang Mar 14, 2024
cb45a39
Merge branch 'main' into feat/icon
JimBobSquarePants Jun 13, 2024
6beeba1
Cleanup and fix issues
JimBobSquarePants Jun 18, 2024
7c0cd0b
Merge separate assert file
JimBobSquarePants Jun 18, 2024
b222a67
Add test files
Poker-sang Jun 18, 2024
bad83f9
Update identifiers
frg2089 Jun 18, 2024
237d129
Update Tests
frg2089 Jun 18, 2024
1f4c7e3
Assert Invalid
frg2089 Jun 18, 2024
9a7727b
Pass the Test
frg2089 Jun 18, 2024
b31bd23
Complete tests and fix issues
JimBobSquarePants Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading