diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 11c6aef294..c26536fd11 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/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()]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); + Color.FromPixel(rgbTable, colorTable); + this.bmpMetadata.ColorTable = colorTable; + } + int skipAmount = 0; if (this.fileHeader.HasValue) { diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index f84e4f9c2e..151da18281 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/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; diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index a2ed1d21d0..a50023b272 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/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(); + } } /// @@ -35,8 +40,11 @@ public class BmpMetadata : IDeepCloneable /// public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; + /// + /// Gets or sets the color table, if any. + /// + public ReadOnlyMemory? ColorTable { get; set; } + /// public IDeepCloneable DeepClone() => new BmpMetadata(this); - - // TODO: Colors used once we support encoding palette bmps. } diff --git a/src/ImageSharp/Formats/Cur/CurConstants.cs b/src/ImageSharp/Formats/Cur/CurConstants.cs index 6efd2817c7..7abf4c812e 100644 --- a/src/ImageSharp/Formats/Cur/CurConstants.cs +++ b/src/ImageSharp/Formats/Cur/CurConstants.cs @@ -9,20 +9,31 @@ namespace SixLabors.ImageSharp.Formats.Cur; internal static class CurConstants { /// - /// The list of mimetypes that equate to a ico. + /// The list of mime types that equate to a cur. /// /// /// See /// - public static readonly IEnumerable MimeTypes = new[] - { - "application/octet-stream", - }; + public static readonly IEnumerable 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", + ]; /// - /// The list of file extensions that equate to a ico. + /// The list of file extensions that equate to a cur. /// - public static readonly IEnumerable FileExtensions = new[] { "cur" }; + public static readonly IEnumerable FileExtensions = ["cur"]; public const uint FileHeader = 0x00_02_00_00; } diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index 538f9a2c6b..18ab8c75ab 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/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; } } diff --git a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs index 3a7288b718..a6922d431a 100644 --- a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs +++ b/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) + { + } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index e8f3cfe8ee..fc5cc5b2cc 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/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, IDeepCloneable { } - /// - /// Initializes a new instance of the class. - /// - /// width - /// height - /// colorCount - /// hotspotX - /// hotspotY - 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; - } - - /// - 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; } /// - /// Gets or sets icoFrameCompression. + /// Gets or sets the frame compressions format. /// public IconFrameCompression Compression { get; set; } /// - /// Gets or sets ColorCount field.
- /// 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. ///
- // TODO: BmpMetadata does not supported palette yet. - public byte ColorCount { get; set; } + public ushort HotspotX { get; set; } /// - /// 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. /// - public ushort HotspotX { get; set; } + public ushort HotspotY { get; set; } /// - /// Gets or sets Specifies the vertical coordinates of the hotspot in number of pixels from the top. + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. ///
- public ushort HotspotY { get; set; } + public byte EncodingWidth { get; set; } /// - /// Gets or sets Height field.
- /// 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.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. ///
public byte EncodingHeight { get; set; } /// - /// Gets or sets Width field.
- /// 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.
+ /// Used when is ///
- public byte EncodingWidth { get; set; } - - /// - public Bmp.BmpBitsPerPixel BitsPerPixel { get; set; } = Bmp.BmpBitsPerPixel.Pixel24; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; /// public CurFrameMetadata DeepClone() => new(this); @@ -88,21 +67,27 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable /// 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 + }; + } } diff --git a/src/ImageSharp/Formats/Ico/IcoConstants.cs b/src/ImageSharp/Formats/Ico/IcoConstants.cs index 0b963a431b..1165793688 100644 --- a/src/ImageSharp/Formats/Ico/IcoConstants.cs +++ b/src/ImageSharp/Formats/Ico/IcoConstants.cs @@ -9,13 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Ico; internal static class IcoConstants { /// - /// The list of mimetypes that equate to a ico. + /// The list of mime types that equate to a ico. /// /// /// See /// - public static readonly IEnumerable MimeTypes = new[] - { + public static readonly IEnumerable MimeTypes = + [ + // IANA-registered "image/vnd.microsoft.icon", @@ -27,12 +28,12 @@ internal static class IcoConstants "image/icon", "text/ico", "application/ico", - }; + ]; /// /// The list of file extensions that equate to a ico. /// - public static readonly IEnumerable FileExtensions = new[] { "ico" }; + public static readonly IEnumerable FileExtensions = ["ico"]; public const uint FileHeader = 0x00_01_00_00; } diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index 78cb0d9615..e8629e35b9 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/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; } } diff --git a/src/ImageSharp/Formats/Ico/IcoEncoder.cs b/src/ImageSharp/Formats/Ico/IcoEncoder.cs index 0668a7e23b..298f93decd 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoder.cs +++ b/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; /// /// Image encoder for writing an image to a stream as a Windows Icon. /// -public sealed class IcoEncoder : QuantizingImageEncoder +public sealed class IcoEncoder : ImageEncoder { /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs index 12ced58fd6..ab3edfbd3c 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs +++ b/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) + { + } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index 8d7eb17b5c..82e4ce3b28 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/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; /// -/// IcoFrameMetadata. TODO: Remove base class and merge into this class. +/// Provides Ico specific metadata information for the image frame. /// public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable { @@ -17,54 +18,36 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable { } - /// - /// Initializes a new instance of the class. - /// - /// width - /// height - /// colorCount - public IcoFrameMetadata(byte width, byte height, byte colorCount) - { - this.EncodingWidth = width; - this.EncodingHeight = height; - this.ColorCount = colorCount; - } - - /// - 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; } /// - /// Gets or sets icoFrameCompression. + /// Gets or sets the frame compressions format. /// public IconFrameCompression Compression { get; set; } /// - /// Gets or sets ColorCount field.
- /// 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.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. ///
- // TODO: BmpMetadata does not supported palette yet. - public byte ColorCount { get; set; } + public byte EncodingWidth { get; set; } /// - /// Gets or sets Height field.
- /// 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.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. ///
public byte EncodingHeight { get; set; } /// - /// Gets or sets Width field.
- /// 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.
+ /// Used when is ///
- public byte EncodingWidth { get; set; } - - /// - public Bmp.BmpBitsPerPixel BitsPerPixel { get; set; } = Bmp.BmpBitsPerPixel.Pixel24; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; /// public IcoFrameMetadata DeepClone() => new(this); @@ -72,24 +55,29 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable /// 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, + }, + }; + } } diff --git a/src/ImageSharp/Formats/Icon/IconAssert.cs b/src/ImageSharp/Formats/Icon/IconAssert.cs index 547b6a6eba..398a3e5f44 100644 --- a/src/ImageSharp/Formats/Icon/IconAssert.cs +++ b/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; - } } diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index a502b91c90..97d0aec6d0 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/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 result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => { - BmpBitsPerPixel bitsPerPixel = default; + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; ImageFrame target = new(this.Options.Configuration, this.Dimensions); ImageFrame 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, + }); } } diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index aa583ee1ef..3e02538c84 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/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); + + /// + /// Reserved. Must always be 0. + /// public ushort Reserved = reserved; + + /// + /// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. + /// public IconFileType Type = type; + + /// + /// Specifies number of images in the file. + /// 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 data) + public static IconDir Parse(ReadOnlySpan data) => MemoryMarshal.Cast(data)[0]; - public unsafe void WriteTo(in Stream stream) + public readonly unsafe void WriteTo(Stream stream) => stream.Write(MemoryMarshal.Cast([this])); } diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs index 7a8e09e37b..eab15dd872 100644 --- a/src/ImageSharp/Formats/Icon/IconDirEntry.cs +++ b/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)); + /// + /// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels. + /// public byte Width; + /// + /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[ + /// public byte Height; + /// + /// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette. + /// public byte ColorCount; + /// + /// Reserved. Should be 0. + /// public byte Reserved; + /// + /// In ICO format: Specifies color planes. Should be 0 or 1.
+ /// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left. + ///
public ushort Planes; + /// + /// In ICO format: Specifies bits per pixel.
+ /// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top. + ///
public ushort BitCount; + /// + /// Specifies the size of the image's data in bytes + /// public uint BytesInRes; + /// + /// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file. + /// public uint ImageOffset; public static IconDirEntry Parse(in ReadOnlySpan data) => MemoryMarshal.Cast(data)[0]; - public unsafe void WriteTo(in Stream stream) + public readonly unsafe void WriteTo(in Stream stream) => stream.Write(MemoryMarshal.Cast([this])); } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index b4d563d4b6..5332d9a860 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/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( Image 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 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 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 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; } } diff --git a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs index b66b6c79fc..9e7d22de22 100644 --- a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs @@ -60,9 +60,7 @@ public class IconImageFormatDetector : IImageFormatDetector return true; } - else - { - return false; - } + + return false; } }