From b31bd2337c57c39a62d4be09c7db68d264b0eea7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 19 Jun 2024 16:24:31 +1000 Subject: [PATCH] Complete tests and fix issues --- src/ImageSharp/Formats/Cur/CurDecoderCore.cs | 8 +- src/ImageSharp/Formats/Cur/CurEncoder.cs | 2 +- src/ImageSharp/Formats/Cur/CurEncoderCore.cs | 4 +- .../Formats/Cur/CurFrameMetadata.cs | 25 ++-- src/ImageSharp/Formats/Ico/IcoDecoderCore.cs | 8 +- src/ImageSharp/Formats/Ico/IcoEncoder.cs | 4 +- src/ImageSharp/Formats/Ico/IcoEncoderCore.cs | 4 +- .../Formats/Ico/IcoFrameMetadata.cs | 26 +++- .../Formats/Icon/IconDecoderCore.cs | 81 ++++++++-- .../Formats/Icon/IconEncoderCore.cs | 72 ++++++--- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 31 +++- .../Quantization/PaletteQuantizer.cs | 7 +- .../Formats/Icon/Cur/CurDecoderTests.cs | 19 ++- .../Formats/Icon/Cur/CurEncoderTests.cs | 26 ++-- .../Formats/Icon/Ico/IcoDecoderTests.cs | 139 ++++++++++++++---- .../Formats/Icon/Ico/IcoEncoderTests.cs | 26 ++-- 16 files changed, 363 insertions(+), 119 deletions(-) diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index 18ab8c75ab..3018ec6bf9 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -14,11 +14,17 @@ internal sealed class CurDecoderCore : IconDecoderCore { } - protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel) + protected override void SetFrameMetadata( + ImageFrameMetadata metadata, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable) { CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); curFrameMetadata.FromIconDirEntry(entry); curFrameMetadata.Compression = compression; curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; + curFrameMetadata.ColorTable = colorTable; } } diff --git a/src/ImageSharp/Formats/Cur/CurEncoder.cs b/src/ImageSharp/Formats/Cur/CurEncoder.cs index d237fe7d0d..e19a73990c 100644 --- a/src/ImageSharp/Formats/Cur/CurEncoder.cs +++ b/src/ImageSharp/Formats/Cur/CurEncoder.cs @@ -11,7 +11,7 @@ public sealed class CurEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - CurEncoderCore encoderCore = new(); + CurEncoderCore encoderCore = new(this); encoderCore.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs index a6922d431a..6435587e2f 100644 --- a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs @@ -7,8 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Cur; internal sealed class CurEncoderCore : IconEncoderCore { - public CurEncoderCore() - : base(IconFileType.CUR) + public CurEncoderCore(QuantizingImageEncoder encoder) + : base(encoder, IconFileType.CUR) { } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index fc5cc5b2cc..014944ba69 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Cur; @@ -18,14 +19,14 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable { } - private CurFrameMetadata(CurFrameMetadata metadata) + private CurFrameMetadata(CurFrameMetadata other) { - this.Compression = metadata.Compression; - this.HotspotX = metadata.HotspotX; - this.HotspotY = metadata.HotspotY; - this.EncodingWidth = metadata.EncodingWidth; - this.EncodingHeight = metadata.EncodingHeight; - this.BmpBitsPerPixel = metadata.BmpBitsPerPixel; + this.Compression = other.Compression; + this.HotspotX = other.HotspotX; + this.HotspotY = other.HotspotY; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; } /// @@ -45,13 +46,13 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable /// /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. + /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
public byte EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. + /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
public byte EncodingHeight { get; set; } @@ -61,6 +62,12 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable ///
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + /// + /// Gets or sets the color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? ColorTable { get; set; } + /// public CurFrameMetadata DeepClone() => new(this); diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index e8629e35b9..f4990c66af 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -14,11 +14,17 @@ internal sealed class IcoDecoderCore : IconDecoderCore { } - protected override void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel) + protected override void SetFrameMetadata( + ImageFrameMetadata metadata, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable) { IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); icoFrameMetadata.FromIconDirEntry(entry); icoFrameMetadata.Compression = compression; icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; + icoFrameMetadata.ColorTable = colorTable; } } diff --git a/src/ImageSharp/Formats/Ico/IcoEncoder.cs b/src/ImageSharp/Formats/Ico/IcoEncoder.cs index 298f93decd..e729251567 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoder.cs +++ b/src/ImageSharp/Formats/Ico/IcoEncoder.cs @@ -6,12 +6,12 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// /// Image encoder for writing an image to a stream as a Windows Icon. /// -public sealed class IcoEncoder : ImageEncoder +public sealed class IcoEncoder : QuantizingImageEncoder { /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - IcoEncoderCore encoderCore = new(); + IcoEncoderCore encoderCore = new(this); encoderCore.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs index ab3edfbd3c..f3cacb1b96 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs @@ -7,8 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Ico; internal sealed class IcoEncoderCore : IconEncoderCore { - public IcoEncoderCore() - : base(IconFileType.ICO) + public IcoEncoderCore(QuantizingImageEncoder encoder) + : base(encoder, IconFileType.ICO) { } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index 82e4ce3b28..ea27d13c8d 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ico; @@ -18,12 +19,17 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable { } - private IcoFrameMetadata(IcoFrameMetadata metadata) + private IcoFrameMetadata(IcoFrameMetadata other) { - this.Compression = metadata.Compression; - this.EncodingWidth = metadata.EncodingWidth; - this.EncodingHeight = metadata.EncodingHeight; - this.BmpBitsPerPixel = metadata.BmpBitsPerPixel; + this.Compression = other.Compression; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } } /// @@ -33,13 +39,13 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable /// /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. + /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
public byte EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels. + /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
public byte EncodingHeight { get; set; } @@ -49,6 +55,12 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable ///
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + /// + /// Gets or sets the color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? ColorTable { get; set; } + /// public IcoFrameMetadata DeepClone() => new(this); diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index a0849fa614..74fe7b9e50 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -31,10 +31,16 @@ internal abstract class IconDecoderCore : IImageDecoderInternals Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; - List<(Image Image, IconFrameCompression Compression, int Index)> decodedEntries = new(this.entries.Length); + List<(Image Image, IconFrameCompression Compression, int Index)> decodedEntries + = new((int)Math.Min(this.entries.Length, this.Options.MaxFrames)); for (int i = 0; i < this.entries.Length; i++) { + if (i == this.Options.MaxFrames) + { + break; + } + ref IconDirEntry entry = ref this.entries[i]; // If we hit the end of the stream we should break. @@ -69,6 +75,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => { BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + ReadOnlyMemory? colorTable = null; ImageFrame target = new(this.Options.Configuration, this.Dimensions); ImageFrame source = x.Image.Frames.RootFrameUnsafe; for (int y = 0; y < source.Height; y++) @@ -88,11 +95,22 @@ internal abstract class IconDecoderCore : IImageDecoderInternals } else { - bmpMetadata = x.Image.Metadata.GetBmpMetadata(); - bitsPerPixel = bmpMetadata.BitsPerPixel; + BmpMetadata meta = x.Image.Metadata.GetBmpMetadata(); + bitsPerPixel = meta.BitsPerPixel; + colorTable = meta.ColorTable; + + if (x.Index == 0) + { + bmpMetadata = meta; + } } - this.SetFrameMetadata(target.Metadata, this.entries[x.Index], x.Compression, bitsPerPixel); + this.SetFrameMetadata( + target.Metadata, + this.entries[x.Index], + x.Compression, + bitsPerPixel, + colorTable); x.Image.Dispose(); @@ -122,11 +140,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; ImageMetadata metadata = new(); - ImageFrameMetadata[] frames = new ImageFrameMetadata[this.fileHeader.Count]; + BmpMetadata? bmpMetadata = null; + PngMetadata? pngMetadata = null; + ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)]; int bpp = 0; for (int i = 0; i < frames.Length; i++) { BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + ReadOnlyMemory? colorTable = null; ref IconDirEntry entry = ref this.entries[i]; // If we hit the end of the stream we should break. @@ -149,25 +170,65 @@ internal abstract class IconDecoderCore : IImageDecoderInternals // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. ImageInfo temp = this.GetDecoder(isPng).Identify(stream, cancellationToken); - frames[i] = new(); - if (!isPng) + ImageFrameMetadata frameMetadata = new(); + + if (isPng) + { + if (i == 0) + { + pngMetadata = temp.Metadata.GetPngMetadata(); + } + + frameMetadata.SetFormatMetadata(PngFormat.Instance, temp.FrameMetadataCollection[0].GetPngMetadata()); + } + else { - bitsPerPixel = temp.Metadata.GetBmpMetadata().BitsPerPixel; + BmpMetadata meta = temp.Metadata.GetBmpMetadata(); + bitsPerPixel = meta.BitsPerPixel; + colorTable = meta.ColorTable; + + if (i == 0) + { + bmpMetadata = meta; + } } bpp = Math.Max(bpp, (int)bitsPerPixel); - this.SetFrameMetadata(frames[i], this.entries[i], isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, bitsPerPixel); + frames[i] = frameMetadata; + + this.SetFrameMetadata( + frames[i], + this.entries[i], + isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, + bitsPerPixel, + colorTable); // 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. this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); } + // Copy the format specific metadata to the image. + if (bmpMetadata != null) + { + metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); + } + + if (pngMetadata != null) + { + metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); + } + return new(new(bpp), this.Dimensions, metadata, frames); } - protected abstract void SetFrameMetadata(ImageFrameMetadata metadata, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel); + protected abstract void SetFrameMetadata( + ImageFrameMetadata metadata, + in IconDirEntry entry, + IconFrameCompression compression, + BmpBitsPerPixel bitsPerPixel, + ReadOnlyMemory? colorTable); [MemberNotNull(nameof(entries))] protected void ReadHeader(Stream stream) diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index eb07ab483b..2433396612 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -13,12 +13,16 @@ namespace SixLabors.ImageSharp.Formats.Icon; internal abstract class IconEncoderCore : IImageEncoderInternals { + private readonly QuantizingImageEncoder encoder; private readonly IconFileType iconFileType; private IconDir fileHeader; private EncodingFrameMetadata[]? entries; - protected IconEncoderCore(IconFileType iconFileType) - => this.iconFileType = iconFileType; + protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType) + { + this.encoder = encoder; + this.iconFileType = iconFileType; + } public void Encode( Image image, @@ -71,14 +75,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals { 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, + Quantizer = this.GetQuantizer(encodingMetadata), ProcessedAlphaMask = true, UseDoubleHeight = true, SkipFileHeader = true, @@ -122,28 +119,65 @@ internal abstract class IconEncoderCore : IImageEncoderInternals image.Frames.Select(i => { IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); }).ToArray(), IconFileType.CUR => image.Frames.Select(i => { CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); }).ToArray(), _ => throw new NotSupportedException(), }; } - internal sealed class EncodingFrameMetadata( - IconFrameCompression compression, - BmpBitsPerPixel bmpBitsPerPixel, - IconDirEntry iconDirEntry) + private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata) { - private IconDirEntry iconDirEntry = iconDirEntry; + if (metadata.Entry.BitCount > 8) + { + return null; + } + + if (this.encoder.Quantizer is not null) + { + return this.encoder.Quantizer; + } + + if (metadata.ColorTable is null) + { + return new WuQuantizer(new() + { + MaxColors = metadata.Entry.ColorCount + }); + } + + // Don't dither if we have a palette. We want to preserve as much information as possible. + return new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }); + } + + internal sealed class EncodingFrameMetadata + { + private IconDirEntry iconDirEntry; + + public EncodingFrameMetadata( + IconFrameCompression compression, + BmpBitsPerPixel bmpBitsPerPixel, + ReadOnlyMemory? colorTable, + IconDirEntry iconDirEntry) + { + this.Compression = compression; + this.BmpBitsPerPixel = compression == IconFrameCompression.Png + ? BmpBitsPerPixel.Pixel32 + : bmpBitsPerPixel; + this.ColorTable = colorTable; + this.iconDirEntry = iconDirEntry; + } + + public IconFrameCompression Compression { get; } - public IconFrameCompression Compression { get; set; } = compression; + public BmpBitsPerPixel BmpBitsPerPixel { get; } - public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = bmpBitsPerPixel; + public ReadOnlyMemory? ColorTable { get; set; } public ref IconDirEntry Entry => ref this.iconDirEntry; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 36a0a8bcbb..3e278be14c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -345,9 +345,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { uint frameCount = 0; ImageMetadata metadata = new(); + List framesMetadata = []; PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; - FrameControl? lastFrameControl = null; + FrameControl? currentFrameControl = null; Span buffer = stackalloc byte[20]; this.currentStream.Skip(8); @@ -400,7 +401,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; case PngChunkType.FrameData: if (frameCount == this.maxFrames) @@ -413,22 +415,35 @@ internal sealed class PngDecoderCore : IImageDecoderInternals goto EOF; } - if (lastFrameControl is null) + if (currentFrameControl is null) { PngThrowHelper.ThrowMissingFrameControl(); } + InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); + // Skip sequence number this.currentStream.Skip(4); this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Data: + // Spec says tRNS must be before IDAT so safe to exit. if (this.colorMetadataOnly) { goto EOF; } + pngMetadata.AnimateRootFrame = currentFrameControl != null; + currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); + if (framesMetadata.Count == 0) + { + InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + } + this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Palette: @@ -515,7 +530,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); - return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata, framesMetadata); } finally { @@ -680,6 +695,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } + private static void InitializeFrameMetadata(List imageFrameMetadata, FrameControl currentFrameControl) + { + ImageFrameMetadata meta = new(); + PngFrameMetadata frameMetadata = meta.GetPngMetadata(); + frameMetadata.FromChunk(currentFrameControl); + imageFrameMetadata.Add(meta); + } + /// /// Calculates the correct number of bits per pixel for the given color type. /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index acd179ffcc..13a59a26de 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -62,9 +62,10 @@ public class PaletteQuantizer : IQuantizer { Guard.NotNull(options, nameof(options)); - // Always use the palette length over options since the palette cannot be reduced. - TPixel[] palette = new TPixel[this.colorPalette.Length]; - Color.ToPixel(this.colorPalette.Span, palette.AsSpan()); + // If the palette is larger than the max colors then we need to trim it down. + // treat the buffer as FILO. + TPixel[] palette = new TPixel[Math.Min(options.MaxColors, this.colorPalette.Length)]; + Color.ToPixel(this.colorPalette.Span[..palette.Length], palette.AsSpan()); return new PaletteQuantizer(configuration, options, palette, this.transparentIndex); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index e6efda4b6d..4efd336482 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Cur; @@ -17,9 +19,11 @@ public class CurDecoderTests { using Image image = provider.GetImage(CurDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); - - // TODO: Assert metadata, frame count, etc + CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); + Assert.Equal(image.Width, meta.EncodingWidth); + Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); } [Theory] @@ -28,9 +32,10 @@ public class CurDecoderTests public void CurDecoder_Decode2(TestImageProvider provider) { using Image image = provider.GetImage(CurDecoder.Instance); - - image.DebugSaveMultiFrame(provider, extension: "png"); - - // TODO: Assert metadata, frame count, etc + CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); + Assert.Equal(image.Width, meta.EncodingWidth); + Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index 9908786f18..b9b66296df 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; @@ -10,23 +11,24 @@ 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 }, - }; + private static CurEncoder Encoder => new(); [Theory] - [MemberData(nameof(Files))] - public void Encode(string imagePath) + [WithFile(CurReal, PixelTypes.Rgba32)] + [WithFile(WindowsMouse, PixelTypes.Rgba32)] + public void CanRoundTripEncoder(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - TestFile testFile = TestFile.Create(imagePath); - using Image input = testFile.CreateRgba32Image(); + using Image image = provider.GetImage(CurDecoder.Instance); using MemoryStream memStream = new(); - input.Save(memStream, CurEncoder); + image.DebugSaveMultiFrame(provider); + image.Save(memStream, Encoder); memStream.Seek(0, SeekOrigin.Begin); - CurDecoder.Instance.Decode(new(), memStream); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); + + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index 1c724ca35b..a776a637b8 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Ico; @@ -17,9 +19,9 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSaveMultiFrame(provider); - // TODO: Assert metadata, frame count, etc + Assert.Equal(10, image.Frames.Count); } [Theory] @@ -45,9 +47,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); - // TODO: Assert metadata, frame count, etc + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; + + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel); } [Theory] @@ -74,9 +83,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); + + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; - // TODO: Assert metadata, frame count, etc + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel); } [Theory] @@ -103,9 +119,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); - // TODO: Assert metadata, frame count, etc + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; + + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); } [Theory] @@ -131,9 +154,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); + + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; - // TODO: Assert metadata, frame count, etc + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel); } [Theory] @@ -151,7 +181,8 @@ public class IcoDecoderTests [WithFile(Bpp8Size5x5, PixelTypes.Rgba32)] [WithFile(Bpp8Size6x6, PixelTypes.Rgba32)] [WithFile(Bpp8Size7x7, PixelTypes.Rgba32)] - [WithFile(Bpp8Size8x8, PixelTypes.Rgba32)] + + // [WithFile(Bpp8Size8x8, PixelTypes.Rgba32)] This is actually 24 bit. [WithFile(Bpp8Size9x9, PixelTypes.Rgba32)] [WithFile(Bpp8TranspNotSquare, PixelTypes.Rgba32)] [WithFile(Bpp8TranspPartial, PixelTypes.Rgba32)] @@ -159,9 +190,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); + + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; - // TODO: Assert metadata, frame count, etc + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel); } [Theory] @@ -174,10 +212,6 @@ public class IcoDecoderTests => Assert.Throws(() => { using Image image = provider.GetImage(IcoDecoder.Instance); - - image.DebugSaveMultiFrame(provider, extension: "png"); - - // TODO: Assert metadata, frame count, etc }); [Theory] @@ -186,9 +220,16 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); + + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; - // TODO: Assert metadata, frame count, etc + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Png, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); } [Theory] @@ -199,9 +240,9 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + Assert.True(image.Frames.Count > 1); - // TODO: Assert metadata, frame count, etc + image.DebugSaveMultiFrame(provider); } [Theory] @@ -215,9 +256,46 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + Assert.True(image.Frames.Count > 1); + + for (int i = 0; i < image.Frames.Count; i++) + { + ImageFrame frame = image.Frames[i]; + IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata(); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + } + + image.DebugSaveMultiFrame(provider); + } + + [Theory] + [WithFile(MultiSizeA, PixelTypes.Rgba32)] + [WithFile(MultiSizeB, PixelTypes.Rgba32)] + [WithFile(MultiSizeC, PixelTypes.Rgba32)] + [WithFile(MultiSizeD, PixelTypes.Rgba32)] + [WithFile(MultiSizeE, PixelTypes.Rgba32)] + [WithFile(MultiSizeF, PixelTypes.Rgba32)] + public void MultiSize_CanDecodeSingleFrame(TestImageProvider provider) + { + using Image image = provider.GetImage(IcoDecoder.Instance, new() { MaxFrames = 1 }); + Assert.Single(image.Frames); + } - // TODO: Assert metadata, frame count, etc + [Theory] + [InlineData(MultiSizeA)] + [InlineData(MultiSizeB)] + [InlineData(MultiSizeC)] + [InlineData(MultiSizeD)] + [InlineData(MultiSizeE)] + [InlineData(MultiSizeF)] + public void MultiSize_CanIdentifySingleFrame(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + ImageInfo imageInfo = Image.Identify(new() { MaxFrames = 1 }, stream); + + Assert.Single(imageInfo.FrameMetadataCollection); } [Theory] @@ -229,9 +307,9 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + Assert.True(image.Frames.Count > 1); - // TODO: Assert metadata, frame count, etc + image.DebugSaveMultiFrame(provider); } [Theory] @@ -240,8 +318,15 @@ public class IcoDecoderTests { using Image image = provider.GetImage(IcoDecoder.Instance); - image.DebugSaveMultiFrame(provider, extension: "png"); + image.DebugSave(provider); + + IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + int expectedWidth = image.Width >= 256 ? 0 : image.Width; + int expectedHeight = image.Height >= 256 ? 0 : image.Height; - // TODO: Assert metadata, frame count, etc + Assert.Equal(expectedWidth, meta.EncodingWidth); + Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(IconFrameCompression.Bmp, meta.Compression); + Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index 9a239bdd41..db28f9f703 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; @@ -10,23 +11,24 @@ 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 }, - }; + private static IcoEncoder Encoder => new(); [Theory] - [MemberData(nameof(Files))] - public void Encode(string imagePath) + [WithFile(Flutter, PixelTypes.Rgba32)] + public void CanRoundTripEncoder(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - TestFile testFile = TestFile.Create(imagePath); - using Image input = testFile.CreateRgba32Image(); + using Image image = provider.GetImage(IcoDecoder.Instance); using MemoryStream memStream = new(); - input.Save(memStream, CurEncoder); + image.DebugSaveMultiFrame(provider); + image.Save(memStream, Encoder); memStream.Seek(0, SeekOrigin.Begin); - IcoDecoder.Instance.Decode(new(), memStream); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); + + // Despite preservation of the palette. The process can still be lossy + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); } }