From dbd178392f21d3cf36d2bb2129193453acb58fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1!?= Date: Thu, 14 Dec 2023 17:34:38 +0800 Subject: [PATCH] Add encoders that cannot be used. --- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 9 +++ src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 81 ++++++++++++++++++- .../Formats/Cur/CurConfigurationModule.cs | 3 +- src/ImageSharp/Formats/Cur/CurEncoder.cs | 17 ++++ src/ImageSharp/Formats/Cur/CurEncoderCore.cs | 19 +++++ .../Formats/Ico/IcoConfigurationModule.cs | 3 +- src/ImageSharp/Formats/Ico/IcoEncoder.cs | 17 ++++ src/ImageSharp/Formats/Ico/IcoEncoderCore.cs | 19 +++++ src/ImageSharp/Formats/Icon/IconAssert.cs | 14 ++++ src/ImageSharp/Formats/Icon/IconDir.cs | 3 + src/ImageSharp/Formats/Icon/IconDirEntry.cs | 3 + .../Formats/Icon/IconEncoderCore.cs | 69 ++++++++++++++++ src/ImageSharp/Metadata/ImageMetadata.cs | 1 + .../Formats/Icon/Cur/CurEncoderTests.cs | 32 ++++++++ .../Formats/Icon/Ico/IcoEncoderTests.cs | 32 ++++++++ 15 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 src/ImageSharp/Formats/Cur/CurEncoder.cs create mode 100644 src/ImageSharp/Formats/Cur/CurEncoderCore.cs create mode 100644 src/ImageSharp/Formats/Ico/IcoEncoder.cs create mode 100644 src/ImageSharp/Formats/Ico/IcoEncoderCore.cs create mode 100644 src/ImageSharp/Formats/Icon/IconEncoderCore.cs create mode 100644 tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs 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); + } +}