diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
index 0081f6a1a..0be243f9a 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
@@ -29,6 +29,15 @@ public sealed class BmpEncoder : QuantizingImageEncoder
///
public bool SupportTransparency { get; init; }
+ ///
+ internal bool ProcessedAlphaMask { get; init; }
+
+ ///
+ internal bool SkipFileHeader { get; init; }
+
+ ///
+ internal bool UseDoubleHeight { get; init; }
+
///
protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
{
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 076d1adf0..b888fa400 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Buffers.Binary;
+using System.IO;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
@@ -11,6 +12,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
+using static System.Net.Mime.MediaTypeNames;
namespace SixLabors.ImageSharp.Formats.Bmp;
@@ -92,6 +94,15 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
///
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
+ ///
+ private readonly bool processedAlphaMask;
+
+ ///
+ private readonly bool skipFileHeader;
+
+ ///
+ private readonly bool isDoubleHeight;
+
///
/// Initializes a new instance of the class.
///
@@ -104,6 +115,9 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
+ this.processedAlphaMask = encoder.ProcessedAlphaMask;
+ this.skipFileHeader = encoder.SkipFileHeader;
+ this.isDoubleHeight = encoder.UseDoubleHeight;
}
///
@@ -154,11 +168,23 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
_ => BmpInfoHeader.SizeV3
};
- BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
+ // for ico/cur encoder.
+ int height = image.Height;
+ if (this.isDoubleHeight)
+ {
+ height <<= 1;
+ }
+
+ BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span buffer = stackalloc byte[infoHeaderSize];
- WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
+ // for ico/cur encoder.
+ if (!this.skipFileHeader)
+ {
+ WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
+ }
+
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image);
WriteColorProfile(stream, iccProfileData, buffer);
@@ -455,6 +481,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{
this.Write8BitColor(configuration, stream, image, colorPalette);
}
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, image);
+ }
}
///
@@ -572,6 +603,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(0);
}
}
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, image);
+ }
}
///
@@ -629,6 +665,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(0);
}
}
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, image);
+ }
}
///
@@ -679,6 +720,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(0);
}
}
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, image);
+ }
}
///
@@ -722,4 +768,35 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(indices);
}
+
+ private static void ProcessedAlphaMask(Stream stream, Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ Rgba32 rgba = default;
+ int arrayWidth = image.Width / 8;
+ int padding = arrayWidth % 4;
+ if (padding is not 0)
+ {
+ padding = 4 - padding;
+ }
+
+ Span mask = stackalloc byte[arrayWidth];
+ for (int y = image.Height - 1; y >= 0; y--)
+ {
+ mask.Clear();
+ for (int x = 0; x < image.Width; x++)
+ {
+ int bit = x % 8;
+ int i = x / 8;
+ TPixel pixel = image[x, y];
+ pixel.ToRgba32(ref rgba);
+ if (rgba.A is not 0)
+ {
+ mask[i] &= unchecked((byte)(0b10000000 >> bit));
+ }
+ }
+
+ stream.Write(mask);
+ }
+ }
}
diff --git a/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
index 1c7db4bab..879b3f112 100644
--- a/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
+++ b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
@@ -13,8 +13,7 @@ public sealed class CurConfigurationModule : IImageFormatConfigurationModule
///
public void Configure(Configuration configuration)
{
- // TODO: CurEncoder
- // configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
+ configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
diff --git a/src/ImageSharp/Formats/Cur/CurEncoder.cs b/src/ImageSharp/Formats/Cur/CurEncoder.cs
new file mode 100644
index 000000000..d237fe7d0
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurEncoder.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+///
+/// Image encoder for writing an image to a stream as a Windows Cursor.
+///
+public sealed class CurEncoder : QuantizingImageEncoder
+{
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ CurEncoderCore encoderCore = new();
+ encoderCore.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs
new file mode 100644
index 000000000..0cf8c97a5
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+internal sealed class CurEncoderCore : IconEncoderCore
+{
+ protected override void GetHeader(in Image image)
+ {
+ this.FileHeader = new(IconFileType.ICO, (ushort)image.Frames.Count);
+ this.Entries = image.Frames.Select(i =>
+ {
+ CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
+ return metadata.ToIconDirEntry();
+ }).ToArray();
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
index b27d91465..224aaa31e 100644
--- a/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
+++ b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
@@ -13,8 +13,7 @@ public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
///
public void Configure(Configuration configuration)
{
- // TODO: IcoEncoder
- // configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
+ configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
diff --git a/src/ImageSharp/Formats/Ico/IcoEncoder.cs b/src/ImageSharp/Formats/Ico/IcoEncoder.cs
new file mode 100644
index 000000000..0668a7e23
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoEncoder.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+///
+/// Image encoder for writing an image to a stream as a Windows Icon.
+///
+public sealed class IcoEncoder : QuantizingImageEncoder
+{
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ IcoEncoderCore encoderCore = new();
+ encoderCore.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
new file mode 100644
index 000000000..6c2f3abe4
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+internal sealed class IcoEncoderCore : IconEncoderCore
+{
+ protected override void GetHeader(in Image image)
+ {
+ this.FileHeader = new(IconFileType.ICO, (ushort)image.Frames.Count);
+ this.Entries = image.Frames.Select(i =>
+ {
+ IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
+ return metadata.ToIconDirEntry();
+ }).ToArray();
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconAssert.cs b/src/ImageSharp/Formats/Icon/IconAssert.cs
index 04a9527b9..547b6a6eb 100644
--- a/src/ImageSharp/Formats/Icon/IconAssert.cs
+++ b/src/ImageSharp/Formats/Icon/IconAssert.cs
@@ -32,4 +32,18 @@ internal class IconAssert
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/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs
index 390a4de65..aa583ee1e 100644
--- a/src/ImageSharp/Formats/Icon/IconDir.cs
+++ b/src/ImageSharp/Formats/Icon/IconDir.cs
@@ -25,4 +25,7 @@ internal struct IconDir(ushort reserved, IconFileType type, ushort count)
public static IconDir Parse(in ReadOnlySpan data)
=> MemoryMarshal.Cast(data)[0];
+
+ public unsafe void WriteTo(in Stream stream)
+ => stream.Write(MemoryMarshal.Cast([this]));
}
diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
index edd778f7e..7a8e09e37 100644
--- a/src/ImageSharp/Formats/Icon/IconDirEntry.cs
+++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
@@ -28,4 +28,7 @@ internal struct IconDirEntry
public static IconDirEntry Parse(in ReadOnlySpan data)
=> MemoryMarshal.Cast(data)[0];
+
+ public 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
new file mode 100644
index 000000000..05a657b51
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+internal abstract class IconEncoderCore : IImageEncoderInternals
+{
+ protected IconDir FileHeader { get; set; }
+
+ protected IconDirEntry[]? Entries { get; set; }
+
+ public void Encode(
+ Image image,
+ Stream stream,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ Guard.NotNull(image, nameof(image));
+ Guard.NotNull(stream, nameof(stream));
+
+ IconAssert.CanSeek(stream);
+
+ // Stream may not at 0.
+ long basePosition = stream.Position;
+ this.GetHeader(image);
+
+ int dataOffset = IconDir.Size + (IconDirEntry.Size * this.Entries.Length);
+ _ = stream.Seek(dataOffset, SeekOrigin.Current);
+
+ for (int i = 0; i < image.Frames.Count; i++)
+ {
+ ImageFrame frame = image.Frames[i];
+ this.Entries[i].ImageOffset = (uint)stream.Position;
+ Image img = new(Configuration.Default, frame.PixelBuffer, new());
+
+ // Note: this encoder are not supported PNG Data.
+ BmpEncoder encoder = new()
+ {
+ ProcessedAlphaMask = true,
+ UseDoubleHeight = true,
+ SkipFileHeader = true,
+ SupportTransparency = false,
+ BitsPerPixel = this.Entries[i].BitCount is 0
+ ? BmpBitsPerPixel.Pixel8
+ : (BmpBitsPerPixel?)this.Entries[i].BitCount
+ };
+
+ encoder.Encode(img, stream);
+ this.Entries[i].BytesInRes = this.Entries[i].ImageOffset - (uint)stream.Position;
+ }
+
+ long endPosition = stream.Position;
+ _ = stream.Seek(basePosition, SeekOrigin.Begin);
+ this.FileHeader.WriteTo(stream);
+ foreach (IconDirEntry entry in this.Entries)
+ {
+ entry.WriteTo(stream);
+ }
+
+ _ = stream.Seek(endPosition, SeekOrigin.Begin);
+ }
+
+ [MemberNotNull(nameof(Entries))]
+ protected abstract void GetHeader(in Image image);
+}
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index d9aba6631..e811cc1f7 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -215,6 +215,7 @@ public sealed class ImageMetadata : IDeepCloneable
metadata = default;
return false;
}
+
internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value)
where TFormatMetadata : class, IDeepCloneable
=> this.formatMetadata[key] = value;
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
new file mode 100644
index 000000000..9908786f1
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.Tests.TestImages.Cur;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
+
+[Trait("Format", "Cur")]
+public class CurEncoderTests
+{
+ private static CurEncoder CurEncoder => new();
+
+ public static readonly TheoryData Files = new()
+ {
+ { WindowsMouse },
+ };
+
+ [Theory]
+ [MemberData(nameof(Files))]
+ public void Encode(string imagePath)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using Image input = testFile.CreateRgba32Image();
+ using MemoryStream memStream = new();
+ input.Save(memStream, CurEncoder);
+
+ memStream.Seek(0, SeekOrigin.Begin);
+ CurDecoder.Instance.Decode(new(), memStream);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
new file mode 100644
index 000000000..9a239bdd4
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Ico;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.Tests.TestImages.Ico;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
+
+[Trait("Format", "Icon")]
+public class IcoEncoderTests
+{
+ private static IcoEncoder CurEncoder => new();
+
+ public static readonly TheoryData Files = new()
+ {
+ { Flutter },
+ };
+
+ [Theory]
+ [MemberData(nameof(Files))]
+ public void Encode(string imagePath)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using Image input = testFile.CreateRgba32Image();
+ using MemoryStream memStream = new();
+ input.Save(memStream, CurEncoder);
+
+ memStream.Seek(0, SeekOrigin.Begin);
+ IcoDecoder.Instance.Decode(new(), memStream);
+ }
+}