From ffde9a9380698c19b693847ad24f7789ccae9207 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Dec 2023 14:13:53 +0800 Subject: [PATCH] Refactor decoder and add notes --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 4 +- .../Formats/Bmp/BmpDecoderOptions.cs | 12 +- .../Formats/Icon/Cur/CurDecoderCore.cs | 2 + .../Formats/Icon/Cur/CurFrameMetadata.cs | 2 +- .../Formats/Icon/Ico/IcoDecoderCore.cs | 2 + .../Formats/Icon/Ico/IcoFrameMetadata.cs | 8 +- src/ImageSharp/Formats/Icon/IconAssert.cs | 8 -- .../Formats/Icon/IconDecoderCore.cs | 107 ++++++++++++------ .../Formats/Icon/IconFrameCompression.cs | 5 - .../Formats/Icon/IconFrameMetadata.cs | 1 + src/ImageSharp/Metadata/ImageFrameMetadata.cs | 6 +- src/ImageSharp/Metadata/ImageMetadata.cs | 3 + .../Formats/Icon/Cur/CurDecoderTests.cs | 7 +- .../Formats/Icon/Ico/IcoDecoderTests.cs | 5 +- 14 files changed, 106 insertions(+), 66 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 9bea6867a..568eea00a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -105,7 +105,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// private readonly bool skipFileHeader; - /// + /// private readonly bool isDoubleHeight; /// @@ -120,7 +120,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals this.memoryAllocator = this.configuration.MemoryAllocator; this.processedAlphaMask = options.ProcessedAlphaMask; this.skipFileHeader = options.SkipFileHeader; - this.isDoubleHeight = options.IsDoubleHeight; + this.isDoubleHeight = options.UseDoubleHeight; } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs index 17d37cab2..158a9d479 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs @@ -18,10 +18,10 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; } /// - /// Gets a value indicating whether the additional AlphaMask is processed at decoding time. + /// Gets a value indicating whether the additional alpha mask is processed at decoding time. /// /// - /// It will be used at IcoDecoder. + /// Used by the icon decoder. /// internal bool ProcessedAlphaMask { get; init; } @@ -29,15 +29,15 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions /// Gets a value indicating whether to skip loading the BMP file header. /// /// - /// It will be used at IcoDecoder. + /// Used by the icon decoder. /// internal bool SkipFileHeader { get; init; } /// - /// Gets a value indicating whether the height is double of true height. + /// Gets a value indicating whether to treat the height as double of true height. /// /// - /// It will be used at IcoDecoder. + /// Used by the icon decoder. /// - internal bool IsDoubleHeight { get; init; } + internal bool UseDoubleHeight { get; init; } } diff --git a/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs index 8b08f127d..44fcc8fcc 100644 --- a/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs @@ -3,6 +3,8 @@ using SixLabors.ImageSharp.Metadata; +// TODO: flatten namespace. +// namespace SixLabors.ImageSharp.Formats.Cur; namespace SixLabors.ImageSharp.Formats.Icon.Cur; internal sealed class CurDecoderCore : IconDecoderCore diff --git a/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs index c94afdd3a..60ced16ea 100644 --- a/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Formats.Icon.Cur; /// -/// IcoFrameMetadata +/// IcoFrameMetadata. TODO: Remove base class and merge into this class. /// public class CurFrameMetadata : IconFrameMetadata, IDeepCloneable, IDeepCloneable { diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs index 0782c2128..677a9c07a 100644 --- a/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs @@ -3,6 +3,8 @@ using SixLabors.ImageSharp.Metadata; +// TODO: flatten namespace. +// namespace SixLabors.ImageSharp.Formats.Ico; namespace SixLabors.ImageSharp.Formats.Icon.Ico; internal sealed class IcoDecoderCore : IconDecoderCore diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs index 7c903facd..4f557715d 100644 --- a/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Formats.Icon.Ico; /// -/// IcoFrameMetadata +/// IcoFrameMetadata. TODO: Remove base class and merge into this class. /// public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable, IDeepCloneable { @@ -38,11 +38,9 @@ public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable - /// Gets or sets Specifies bits per pixel. + /// Gets or sets the bits per pixel. + /// TODO: This needs to be constrained and calculated using the metadata returned from the png/bmp. /// - /// - /// It may used by Encoder. - /// public ushort BitCount { get => this.Field2; set => this.Field2 = value; } /// diff --git a/src/ImageSharp/Formats/Icon/IconAssert.cs b/src/ImageSharp/Formats/Icon/IconAssert.cs index bcb427c1a..04a9527b9 100644 --- a/src/ImageSharp/Formats/Icon/IconAssert.cs +++ b/src/ImageSharp/Formats/Icon/IconAssert.cs @@ -32,12 +32,4 @@ internal class IconAssert return v; } - - internal static void NotSquare(in Size size) - { - if (size.Width != size.Height) - { - throw new FormatException("This image is not square."); - } - } } diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index d9a578ff2..d7fc25c69 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -2,7 +2,8 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; -using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -30,50 +31,63 @@ internal abstract class IconDecoderCore : IImageDecoderInternals long basePosition = stream.Position; this.ReadHeader(stream); - Span flag = stackalloc byte[Png.PngConstants.HeaderBytes.Length]; + Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; + Image result = new(this.Dimensions.Width, this.Dimensions.Height); - List<(Image Image, bool IsPng, int Index)> frames = new(this.Entries.Length); for (int i = 0; i < this.Entries.Length; i++) { - _ = IconAssert.EndOfStream(stream.Seek(basePosition + this.Entries[i].ImageOffset, SeekOrigin.Begin), basePosition + this.Entries[i].ImageOffset); - _ = IconAssert.EndOfStream(stream.Read(flag), Png.PngConstants.HeaderBytes.Length); - _ = stream.Seek(-Png.PngConstants.HeaderBytes.Length, SeekOrigin.Current); + ref IconDirEntry entry = ref this.Entries[i]; - bool isPng = flag.SequenceEqual(Png.PngConstants.HeaderBytes); + // If we hit the end of the stream we should break. + if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) + { + break; + } - Image img = this.GetDecoder(isPng).Decode(stream, cancellationToken); - IconAssert.NotSquare(img.Size); - frames.Add((img, isPng, i)); - if (isPng && img.Size.Width > this.Dimensions.Width) + // There should always be enough bytes for this regardless of the entry type. + if (stream.Read(flag) != PngConstants.HeaderBytes.Length) { - this.Dimensions = img.Size; + break; } - } - ImageMetadata metadata = new(); - return new(this.Options.Configuration, metadata, frames.Select(i => - { - ImageFrame target = new(this.Options.Configuration, this.Dimensions); - ImageFrame source = i.Image.Frames.RootFrameUnsafe; + // Reset the stream position. + stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); + + bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + using Image temp = this.GetDecoder(isPng).Decode(stream, cancellationToken); + + ImageFrame source = temp.Frames.RootFrameUnsafe; + ImageFrame target = i == 0 ? result.Frames.RootFrameUnsafe : result.Frames.CreateFrame(); + + // Draw the new frame at position 0,0. We capture the dimensions for cropping during encoding + // via the icon entry. for (int h = 0; h < source.Height; h++) { source.PixelBuffer.DangerousGetRowSpan(h).CopyTo(target.PixelBuffer.DangerousGetRowSpan(h)); } - if (i.IsPng) + // Copy the format specific metadata to the image. + if (isPng) { - target.Metadata.UnsafeSetFormatMetadata(Png.PngFormat.Instance, i.Image.Metadata.GetPngMetadata()); + if (i == 0) + { + result.Metadata.SetFormatMetadata(PngFormat.Instance, temp.Metadata.GetPngMetadata()); + } + + target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngFrameMetadata()); } - else + else if (i == 0) { - target.Metadata.UnsafeSetFormatMetadata(Bmp.BmpFormat.Instance, i.Image.Metadata.GetBmpMetadata()); + // Bmp does not contain frame specific metadata. + result.Metadata.SetFormatMetadata(BmpFormat.Instance, temp.Metadata.GetBmpMetadata()); } - this.GetFrameMetadata(target.Metadata).FromIconDirEntry(this.Entries[i.Index]); + // TODO: The inheriting decoder should be responsible for setting the actual data (FromIconDirEntry) + // so we can avoid the protected Field1 and Field2 properties and use strong typing. + this.GetFrameMetadata(target.Metadata).FromIconDirEntry(entry); + } - i.Image.Dispose(); - return target; - }).ToArray()); + return result; } public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) @@ -84,11 +98,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals ImageFrameMetadata[] frames = new ImageFrameMetadata[this.FileHeader.Count]; for (int i = 0; i < frames.Length; i++) { + // TODO: Use the Identify methods in each decoder to return the + // format specific metadata for the image and frame. frames[i] = new(); IconFrameMetadata icoFrameMetadata = this.GetFrameMetadata(frames[i]); icoFrameMetadata.FromIconDirEntry(this.Entries[i]); } + // TODO: Use real values from the metadata. return new(new(32), new(0), metadata, frames); } @@ -96,6 +113,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals protected void ReadHeader(Stream stream) { + // TODO: Check length and throw if the header cannot be read. _ = Read(stream, out this.fileHeader, IconDir.Size); this.Entries = new IconDirEntry[this.FileHeader.Count]; for (int i = 0; i < this.Entries.Length; i++) @@ -103,15 +121,39 @@ internal abstract class IconDecoderCore : IImageDecoderInternals _ = Read(stream, out this.Entries[i], IconDirEntry.Size); } - this.Dimensions = new( - this.Entries.Max(i => i.Width), - this.Entries.Max(i => i.Height)); + int width = 0; + int height = 0; + foreach (IconDirEntry entry in this.Entries) + { + if (entry.Width == 0) + { + width = 256; + } + + if (entry.Height == 0) + { + height = 256; + } + + if (width == 256 && height == 256) + { + break; + } + + width = Math.Max(width, entry.Width); + height = Math.Max(height, entry.Height); + } + + this.Dimensions = new(width, height); } private static int Read(Stream stream, out T data, int size) where T : unmanaged { + // TODO: Use explicit parsing methods for each T type. + // See PngHeader.Parse() for example. Span buffer = stackalloc byte[size]; + _ = IconAssert.EndOfStream(stream.Read(buffer), size); data = MemoryMarshal.Cast(buffer)[0]; return size; @@ -121,15 +163,16 @@ internal abstract class IconDecoderCore : IImageDecoderInternals { if (isPng) { - return new Png.PngDecoderCore(this.Options); + return new PngDecoderCore(this.Options); } else { - return new Bmp.BmpDecoderCore(new() + return new BmpDecoderCore(new() { + GeneralOptions = this.Options, ProcessedAlphaMask = true, SkipFileHeader = true, - IsDoubleHeight = true, + UseDoubleHeight = true, }); } } diff --git a/src/ImageSharp/Formats/Icon/IconFrameCompression.cs b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs index f6509f40c..5c772c3fe 100644 --- a/src/ImageSharp/Formats/Icon/IconFrameCompression.cs +++ b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs @@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Icon; /// public enum IconFrameCompression { - /// - /// Unknown - /// - Unknown, - /// /// Bmp /// diff --git a/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs b/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs index 17e641c71..5c23388e7 100644 --- a/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs +++ b/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs @@ -5,6 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Icon; /// /// IconFrameMetadata +/// TODO: Delete this. Treat the individual metadata types as separate types so we can avoid Field1, Field2 and use strong typing with constaints. /// public abstract class IconFrameMetadata : IDeepCloneable { diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 0567c8916..562e47803 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -99,9 +99,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable return newMeta; } - internal void UnsafeSetFormatMetadata( - IImageFormat key, - IDeepCloneable value) + internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) + where TFormatMetadata : class + where TFormatFrameMetadata : class, IDeepCloneable => this.formatMetadata[key] = value; /// diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 6b62be08f..d9aba6631 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -215,6 +215,9 @@ public sealed class ImageMetadata : IDeepCloneable metadata = default; return false; } + internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value) + where TFormatMetadata : class, IDeepCloneable + => this.formatMetadata[key] = value; /// public ImageMetadata DeepClone() => new(this); diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index c3c9ad1c4..f78c04cdf 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Icon.Cur; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Cur; @@ -18,6 +17,10 @@ public class CurDecoderTests { using Image image = provider.GetImage(CurDecoder.Instance); - image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder()); + image.DebugSaveMultiFrame(provider, extension: "png"); + + image.DebugSaveMultiFrame(provider, extension: "png"); + + // TODO: Assert metadata, frame count, etc } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index 6e7b35111..86f8b003b 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Icon.Ico; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Ico; @@ -18,6 +17,8 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder()); + image.DebugSaveMultiFrame(provider, extension: "png"); + + // TODO: Assert metadata, frame count, etc } }