Browse Source

Cleanup and fix issues

pull/2579/head
James Jackson-South 2 years ago
parent
commit
6beeba1575
  1. 9
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  3. 14
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  4. 25
      src/ImageSharp/Formats/Cur/CurConstants.cs
  5. 12
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  6. 6
      src/ImageSharp/Formats/Cur/CurEncoderCore.cs
  7. 83
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  8. 11
      src/ImageSharp/Formats/Ico/IcoConstants.cs
  9. 12
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  10. 4
      src/ImageSharp/Formats/Ico/IcoEncoder.cs
  11. 6
      src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
  12. 76
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  13. 32
      src/ImageSharp/Formats/Icon/IconAssert.cs
  14. 44
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  15. 16
      src/ImageSharp/Formats/Icon/IconDir.cs
  16. 28
      src/ImageSharp/Formats/Icon/IconDirEntry.cs
  17. 73
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  18. 6
      src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs

9
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
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;
@ -1599,6 +1600,14 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
}
}
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)
{

2
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -109,6 +109,8 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;

14
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp;
@ -23,6 +23,11 @@ public class BmpMetadata : IDeepCloneable
{
this.BitsPerPixel = other.BitsPerPixel;
this.InfoHeaderType = other.InfoHeaderType;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
}
/// <summary>
@ -35,8 +40,11 @@ public class BmpMetadata : IDeepCloneable
/// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
/// <summary>
/// Gets or sets the color table, if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new BmpMetadata(this);
// TODO: Colors used once we support encoding palette bmps.
}

25
src/ImageSharp/Formats/Cur/CurConstants.cs

@ -9,20 +9,31 @@ namespace SixLabors.ImageSharp.Formats.Cur;
internal static class CurConstants
{
/// <summary>
/// The list of mimetypes that equate to a ico.
/// The list of mime types that equate to a cur.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
/// </remarks>
public static readonly IEnumerable<string> MimeTypes = new[]
{
"application/octet-stream",
};
public static readonly IEnumerable<string> MimeTypes =
[
// IANA-registered
"image/vnd.microsoft.icon",
// ICO & CUR types used by Windows
"image/x-icon",
// Erroneous types but have been used
"image/ico",
"image/icon",
"text/ico",
"application/ico",
];
/// <summary>
/// The list of file extensions that equate to a ico.
/// The list of file extensions that equate to a cur.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "cur" };
public static readonly IEnumerable<string> FileExtensions = ["cur"];
public const uint FileHeader = 0x00_02_00_00;
}

12
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -1,18 +1,24 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Cur;
internal sealed class CurDecoderCore(DecoderOptions options) : IconDecoderCore(options)
internal sealed class CurDecoderCore : IconDecoderCore
{
protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, Bmp.BmpBitsPerPixel bitsPerPixel)
public CurDecoderCore(DecoderOptions options)
: base(options)
{
}
protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel)
{
CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata();
curFrameMetadata.FromIconDirEntry(entry);
curFrameMetadata.Compression = compression;
curFrameMetadata.BitsPerPixel = bitsPerPixel;
curFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
}
}

6
src/ImageSharp/Formats/Cur/CurEncoderCore.cs

@ -5,6 +5,10 @@ using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Cur;
internal sealed class CurEncoderCore() : IconEncoderCore(IconFileType.CUR)
internal sealed class CurEncoderCore : IconEncoderCore
{
public CurEncoderCore()
: base(IconFileType.CUR)
{
}
}

83
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Cur;
@ -17,70 +18,48 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="colorCount">colorCount</param>
/// <param name="hotspotX">hotspotX</param>
/// <param name="hotspotY">hotspotY</param>
public CurFrameMetadata(byte width, byte height, byte colorCount, ushort hotspotX, ushort hotspotY)
{
this.EncodingWidth = width;
this.EncodingHeight = height;
this.ColorCount = colorCount;
this.HotspotX = hotspotX;
this.HotspotY = hotspotY;
}
/// <inheritdoc cref="CurFrameMetadata()"/>
public CurFrameMetadata(CurFrameMetadata metadata)
private CurFrameMetadata(CurFrameMetadata metadata)
{
this.EncodingWidth = metadata.EncodingWidth;
this.EncodingHeight = metadata.EncodingHeight;
this.ColorCount = metadata.ColorCount;
this.Compression = metadata.Compression;
this.HotspotX = metadata.HotspotX;
this.HotspotY = metadata.HotspotY;
this.Compression = metadata.Compression;
this.EncodingWidth = metadata.EncodingWidth;
this.EncodingHeight = metadata.EncodingHeight;
this.BmpBitsPerPixel = metadata.BmpBitsPerPixel;
}
/// <summary>
/// Gets or sets icoFrameCompression.
/// Gets or sets the frame compressions format.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets ColorCount field. <br />
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left.
/// </summary>
// TODO: BmpMetadata does not supported palette yet.
public byte ColorCount { get; set; }
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top.
/// </summary>
public ushort HotspotX { get; set; }
public ushort HotspotY { get; set; }
/// <summary>
/// Gets or sets Specifies the vertical coordinates of the hotspot in number of pixels from the top.
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels.
/// </summary>
public ushort HotspotY { get; set; }
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets Height field. <br />
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets Width field. <br />
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public byte EncodingWidth { get; set; }
/// <inheritdoc cref="Bmp.BmpMetadata.BitsPerPixel" />
public Bmp.BmpBitsPerPixel BitsPerPixel { get; set; } = Bmp.BmpBitsPerPixel.Pixel24;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
@ -88,21 +67,27 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
internal void FromIconDirEntry(in IconDirEntry entry)
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
this.EncodingHeight = entry.Height;
this.ColorCount = entry.ColorCount;
this.HotspotX = entry.Planes;
this.HotspotY = entry.BitCount;
}
internal IconDirEntry ToIconDirEntry() => new()
internal IconDirEntry ToIconDirEntry()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
ColorCount = this.ColorCount,
Planes = this.HotspotX,
BitCount = this.HotspotY,
};
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8
? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
return new()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Planes = this.HotspotX,
BitCount = this.HotspotY,
ColorCount = colorCount
};
}
}

11
src/ImageSharp/Formats/Ico/IcoConstants.cs

@ -9,13 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Ico;
internal static class IcoConstants
{
/// <summary>
/// The list of mimetypes that equate to a ico.
/// The list of mime types that equate to a ico.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
/// </remarks>
public static readonly IEnumerable<string> MimeTypes = new[]
{
public static readonly IEnumerable<string> MimeTypes =
[
// IANA-registered
"image/vnd.microsoft.icon",
@ -27,12 +28,12 @@ internal static class IcoConstants
"image/icon",
"text/ico",
"application/ico",
};
];
/// <summary>
/// The list of file extensions that equate to a ico.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "ico" };
public static readonly IEnumerable<string> FileExtensions = ["ico"];
public const uint FileHeader = 0x00_01_00_00;
}

12
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -1,18 +1,24 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Ico;
internal sealed class IcoDecoderCore(DecoderOptions options) : IconDecoderCore(options)
internal sealed class IcoDecoderCore : IconDecoderCore
{
protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, Bmp.BmpBitsPerPixel bitsPerPixel)
public IcoDecoderCore(DecoderOptions options)
: base(options)
{
}
protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel)
{
IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata();
icoFrameMetadata.FromIconDirEntry(entry);
icoFrameMetadata.Compression = compression;
icoFrameMetadata.BitsPerPixel = bitsPerPixel;
icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
}
}

4
src/ImageSharp/Formats/Ico/IcoEncoder.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ico;
@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Image encoder for writing an image to a stream as a Windows Icon.
/// </summary>
public sealed class IcoEncoder : QuantizingImageEncoder
public sealed class IcoEncoder : ImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)

6
src/ImageSharp/Formats/Ico/IcoEncoderCore.cs

@ -5,6 +5,10 @@ using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Ico;
internal sealed class IcoEncoderCore() : IconEncoderCore(IconFileType.ICO)
internal sealed class IcoEncoderCore : IconEncoderCore
{
public IcoEncoderCore()
: base(IconFileType.ICO)
{
}
}

76
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -1,12 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// IcoFrameMetadata. TODO: Remove base class and merge into this class.
/// Provides Ico specific metadata information for the image frame.
/// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
{
@ -17,54 +18,36 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="colorCount">colorCount</param>
public IcoFrameMetadata(byte width, byte height, byte colorCount)
{
this.EncodingWidth = width;
this.EncodingHeight = height;
this.ColorCount = colorCount;
}
/// <inheritdoc cref="IcoFrameMetadata()"/>
public IcoFrameMetadata(IcoFrameMetadata metadata)
private IcoFrameMetadata(IcoFrameMetadata metadata)
{
this.Compression = metadata.Compression;
this.EncodingWidth = metadata.EncodingWidth;
this.EncodingHeight = metadata.EncodingHeight;
this.ColorCount = metadata.ColorCount;
this.Compression = metadata.Compression;
this.BmpBitsPerPixel = metadata.BmpBitsPerPixel;
}
/// <summary>
/// Gets or sets icoFrameCompression.
/// Gets or sets the frame compressions format.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets ColorCount field. <br />
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels.
/// </summary>
// TODO: BmpMetadata does not supported palette yet.
public byte ColorCount { get; set; }
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets Height field. <br />
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets Width field. <br />
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public byte EncodingWidth { get; set; }
/// <inheritdoc cref="Bmp.BmpMetadata.BitsPerPixel" />
public Bmp.BmpBitsPerPixel BitsPerPixel { get; set; } = Bmp.BmpBitsPerPixel.Pixel24;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
@ -72,24 +55,29 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
internal void FromIconDirEntry(in IconDirEntry entry)
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
this.EncodingHeight = entry.Height;
this.ColorCount = entry.ColorCount;
}
internal IconDirEntry ToIconDirEntry() => new()
internal IconDirEntry ToIconDirEntry()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
ColorCount = this.ColorCount,
Planes = 1,
BitCount = this.Compression switch
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8
? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
return new()
{
IconFrameCompression.Bmp => (ushort)this.BitsPerPixel,
IconFrameCompression.Png => 32,
_ => throw new NotSupportedException($"Value: {this.Compression}"),
},
};
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Planes = 1,
ColorCount = colorCount,
BitCount = this.Compression switch
{
IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel,
IconFrameCompression.Png or _ => 32,
},
};
}
}

32
src/ImageSharp/Formats/Icon/IconAssert.cs

@ -5,14 +5,6 @@ namespace SixLabors.ImageSharp.Formats.Icon;
internal class IconAssert
{
internal static void CanSeek(Stream stream)
{
if (!stream.CanSeek)
{
throw new NotSupportedException("This stream cannot support seek");
}
}
internal static int EndOfStream(int v, int length)
{
if (v != length)
@ -22,28 +14,4 @@ internal class IconAssert
return v;
}
internal static long EndOfStream(long v, long length)
{
if (v != length)
{
throw new EndOfStreamException();
}
return v;
}
internal static byte Is1ByteSize(int i)
{
if (i is 256)
{
return 0;
}
else if (i > byte.MaxValue)
{
throw new FormatException("Image size Too Large.");
}
return (byte)i;
}
}

44
src/ImageSharp/Formats/Icon/IconDecoderCore.cs

@ -10,12 +10,15 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderInternals
internal abstract class IconDecoderCore : IImageDecoderInternals
{
private IconDir fileHeader;
private IconDirEntry[]? entries;
public DecoderOptions Options { get; } = options;
protected IconDecoderCore(DecoderOptions options)
=> this.Options = options;
public DecoderOptions Options { get; }
public Size Dimensions { get; private set; }
@ -61,10 +64,11 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
}
ImageMetadata metadata = new();
BmpMetadata? bmpMetadata = null;
PngMetadata? pngMetadata = null;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{
BmpBitsPerPixel bitsPerPixel = default;
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe;
for (int y = 0; y < source.Height; y++)
@ -80,12 +84,12 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
pngMetadata = x.Image.Metadata.GetPngMetadata();
}
// Bmp does not contain frame specific metadata.
target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngMetadata());
}
else
{
bitsPerPixel = x.Image.Metadata.GetBmpMetadata().BitsPerPixel;
bmpMetadata = x.Image.Metadata.GetBmpMetadata();
bitsPerPixel = bmpMetadata.BitsPerPixel;
}
this.SetFrameMetadata(target.Metadata, this.entries[x.Index], x.Compression, bitsPerPixel);
@ -96,6 +100,11 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
}).ToArray());
// Copy the format specific metadata to the image.
if (bmpMetadata != null)
{
result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
}
if (pngMetadata != null)
{
result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
@ -114,9 +123,10 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
ImageMetadata metadata = new();
ImageFrameMetadata[] frames = new ImageFrameMetadata[this.fileHeader.Count];
int bpp = 0;
for (int i = 0; i < frames.Length; i++)
{
BmpBitsPerPixel bitsPerPixel = default;
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
ref IconDirEntry entry = ref this.entries[i];
// If we hit the end of the stream we should break.
@ -140,11 +150,13 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
ImageInfo temp = this.GetDecoder(isPng).Identify(stream, cancellationToken);
frames[i] = new();
if (isPng)
if (!isPng)
{
bitsPerPixel = temp.Metadata.GetBmpMetadata().BitsPerPixel;
}
bpp = Math.Max(bpp, (int)bitsPerPixel);
this.SetFrameMetadata(frames[i], this.entries[i], isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, bitsPerPixel);
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
@ -152,7 +164,7 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height));
}
return new(new(32), this.Dimensions, metadata, frames);
return new(new(bpp), this.Dimensions, metadata, frames);
}
protected abstract void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel);
@ -211,15 +223,13 @@ internal abstract class IconDecoderCore(DecoderOptions options) : IImageDecoderI
GeneralOptions = this.Options,
});
}
else
return new BmpDecoderCore(new()
{
return new BmpDecoderCore(new()
{
GeneralOptions = this.Options,
ProcessedAlphaMask = true,
SkipFileHeader = true,
UseDoubleHeight = true,
});
}
GeneralOptions = this.Options,
ProcessedAlphaMask = true,
SkipFileHeader = true,
UseDoubleHeight = true,
});
}
}

16
src/ImageSharp/Formats/Icon/IconDir.cs

@ -9,8 +9,20 @@ namespace SixLabors.ImageSharp.Formats.Icon;
internal struct IconDir(ushort reserved, IconFileType type, ushort count)
{
public const int Size = 3 * sizeof(ushort);
/// <summary>
/// Reserved. Must always be 0.
/// </summary>
public ushort Reserved = reserved;
/// <summary>
/// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid.
/// </summary>
public IconFileType Type = type;
/// <summary>
/// Specifies number of images in the file.
/// </summary>
public ushort Count = count;
public IconDir(IconFileType type)
@ -23,9 +35,9 @@ internal struct IconDir(ushort reserved, IconFileType type, ushort count)
{
}
public static IconDir Parse(in ReadOnlySpan<byte> data)
public static IconDir Parse(ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDir>(data)[0];
public unsafe void WriteTo(in Stream stream)
public readonly unsafe void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDir, byte>([this]));
}

28
src/ImageSharp/Formats/Icon/IconDirEntry.cs

@ -10,25 +10,51 @@ internal struct IconDirEntry
{
public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint));
/// <summary>
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
/// </summary>
public byte Width;
/// <summary>
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[
/// </summary>
public byte Height;
/// <summary>
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
/// </summary>
public byte ColorCount;
/// <summary>
/// Reserved. Should be 0.
/// </summary>
public byte Reserved;
/// <summary>
/// In ICO format: Specifies color planes. Should be 0 or 1.<br/>
/// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
/// </summary>
public ushort Planes;
/// <summary>
/// In ICO format: Specifies bits per pixel.<br/>
/// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
/// </summary>
public ushort BitCount;
/// <summary>
/// Specifies the size of the image's data in bytes
/// </summary>
public uint BytesInRes;
/// <summary>
/// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file.
/// </summary>
public uint ImageOffset;
public static IconDirEntry Parse(in ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDirEntry>(data)[0];
public unsafe void WriteTo(in Stream stream)
public readonly unsafe void WriteTo(in Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDirEntry, byte>([this]));
}

73
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -2,18 +2,23 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconEncoderCore(IconFileType iconFileType)
: IImageEncoderInternals
internal abstract class IconEncoderCore : IImageEncoderInternals
{
private readonly IconFileType iconFileType;
private IconDir fileHeader;
private EncodingFrameMetadata[]? entries;
private IconFrameMetadata[]? entries;
protected IconEncoderCore(IconFileType iconFileType)
=> this.iconFileType = iconFileType;
public void Encode<TPixel>(
Image<TPixel> image,
@ -24,17 +29,18 @@ internal abstract class IconEncoderCore(IconFileType iconFileType)
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
IconAssert.CanSeek(stream);
// Stream may not at 0.
long basePosition = stream.Position;
this.InitHeader(image);
// We don't write the header and entries yet as we need to write the image data first.
int dataOffset = IconDir.Size + (IconDirEntry.Size * this.entries.Length);
_ = stream.Seek(dataOffset, SeekOrigin.Current);
for (int i = 0; i < image.Frames.Count; i++)
{
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
ImageFrame<TPixel> frame = image.Frames[i];
int width = this.entries[i].Entry.Width;
if (width is 0)
@ -50,36 +56,54 @@ internal abstract class IconEncoderCore(IconFileType iconFileType)
this.entries[i].Entry.ImageOffset = (uint)stream.Position;
Image<TPixel> img = new(width, height);
// We crop the frame to the size specified in the metadata.
// TODO: we can optimize this by cropping the frame only if the new size is both required and different.
using Image<TPixel> encodingFrame = new(width, height);
for (int y = 0; y < height; y++)
{
frame.PixelBuffer.DangerousGetRowSpan(y)[..width].CopyTo(img.GetRootFramePixelBuffer().DangerousGetRowSpan(y));
frame.PixelBuffer.DangerousGetRowSpan(y)[..width]
.CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y));
}
QuantizingImageEncoder encoder = this.entries[i].Compression switch
ref EncodingFrameMetadata encodingMetadata = ref this.entries[i];
QuantizingImageEncoder encoder = encodingMetadata.Compression switch
{
IconFrameCompression.Bmp => new Bmp.BmpEncoder()
IconFrameCompression.Bmp => new BmpEncoder()
{
// We don't have access to the palette in the metadata so we need to quantize the image
// using a new one generated from the pixel data.
Quantizer = encodingMetadata.Entry.BitCount <= 8
? new WuQuantizer(new()
{
MaxColors = encodingMetadata.Entry.ColorCount
})
: null,
ProcessedAlphaMask = true,
UseDoubleHeight = true,
SkipFileHeader = true,
SupportTransparency = false,
BitsPerPixel = iconFileType is IconFileType.ICO
? (Bmp.BmpBitsPerPixel?)this.entries[i].Entry.BitCount
: Bmp.BmpBitsPerPixel.Pixel24 // TODO: Here you need to switch to selecting the corresponding value according to the size of the image
BitsPerPixel = encodingMetadata.BmpBitsPerPixel
},
IconFrameCompression.Png => new PngEncoder()
{
// Only 32bit Png supported.
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
BitDepth = PngBitDepth.Bit8,
ColorType = PngColorType.RgbWithAlpha
},
IconFrameCompression.Png => new Png.PngEncoder(),
_ => throw new NotSupportedException(),
};
encoder.Encode(img, stream);
this.entries[i].Entry.BytesInRes = (uint)stream.Position - this.entries[i].Entry.ImageOffset;
encoder.Encode(encodingFrame, stream);
encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset;
}
// We now need to rewind the stream and write the header and the entries.
long endPosition = stream.Position;
_ = stream.Seek(basePosition, SeekOrigin.Begin);
this.fileHeader.WriteTo(stream);
foreach (IconFrameMetadata frame in this.entries)
foreach (EncodingFrameMetadata frame in this.entries)
{
frame.Entry.WriteTo(stream);
}
@ -88,33 +112,38 @@ internal abstract class IconEncoderCore(IconFileType iconFileType)
}
[MemberNotNull(nameof(entries))]
private void InitHeader(in Image image)
private void InitHeader(Image image)
{
this.fileHeader = new(iconFileType, (ushort)image.Frames.Count);
this.entries = iconFileType switch
this.fileHeader = new(this.iconFileType, (ushort)image.Frames.Count);
this.entries = this.iconFileType switch
{
IconFileType.ICO =>
image.Frames.Select(i =>
{
IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
return new IconFrameMetadata(metadata.Compression, metadata.ToIconDirEntry());
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry());
}).ToArray(),
IconFileType.CUR =>
image.Frames.Select(i =>
{
CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
return new IconFrameMetadata(metadata.Compression, metadata.ToIconDirEntry());
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry());
}).ToArray(),
_ => throw new NotSupportedException(),
};
}
internal sealed class IconFrameMetadata(IconFrameCompression compression, IconDirEntry iconDirEntry)
internal sealed class EncodingFrameMetadata(
IconFrameCompression compression,
BmpBitsPerPixel bmpBitsPerPixel,
IconDirEntry iconDirEntry)
{
private IconDirEntry iconDirEntry = iconDirEntry;
public IconFrameCompression Compression { get; set; } = compression;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = bmpBitsPerPixel;
public ref IconDirEntry Entry => ref this.iconDirEntry;
}
}

6
src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs

@ -60,9 +60,7 @@ public class IconImageFormatDetector : IImageFormatDetector
return true;
}
else
{
return false;
}
return false;
}
}

Loading…
Cancel
Save