From 14ced6f6d2594cba4a54711b134301c96407e841 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Mar 2024 12:02:45 +0100 Subject: [PATCH 01/43] Set YcbcrSubSampling to 1, 1 with jpeg compression and PhotometricInterpretation YCbCr --- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 5a5c2b3e51..7172bd6fe6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -628,6 +628,12 @@ internal static class TiffDecoderOptionsParser options.ColorType = TiffColorType.Rgb; } + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + options.YcbcrSubSampling[0] = 1; + options.YcbcrSubSampling[1] = 1; + } + break; } @@ -642,6 +648,12 @@ internal static class TiffDecoderOptionsParser options.ColorType = TiffColorType.Rgb; } + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + options.YcbcrSubSampling[0] = 1; + options.YcbcrSubSampling[1] = 1; + } + break; } From 363769d28220ec8117ecee63f7c0a034b17aedbb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 12 Mar 2024 12:21:58 +0100 Subject: [PATCH 02/43] Avoid code duplication --- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 7172bd6fe6..7b0f439b1c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -620,19 +620,7 @@ internal static class TiffDecoderOptionsParser } options.CompressionType = TiffDecoderCompressionType.OldJpeg; - - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) - { - // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. - options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - options.ColorType = TiffColorType.Rgb; - } - - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) - { - options.YcbcrSubSampling[0] = 1; - options.YcbcrSubSampling[1] = 1; - } + AdjustOptionsYCbCrJpegCompression(options); break; } @@ -640,19 +628,7 @@ internal static class TiffDecoderOptionsParser case TiffCompression.Jpeg: { options.CompressionType = TiffDecoderCompressionType.Jpeg; - - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) - { - // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. - options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - options.ColorType = TiffColorType.Rgb; - } - - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) - { - options.YcbcrSubSampling[0] = 1; - options.YcbcrSubSampling[1] = 1; - } + AdjustOptionsYCbCrJpegCompression(options); break; } @@ -671,6 +647,24 @@ internal static class TiffDecoderOptionsParser } } + private static void AdjustOptionsYCbCrJpegCompression(TiffDecoderCore options) + { + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } + + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this, + // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679 + options.YcbcrSubSampling[0] = 1; + options.YcbcrSubSampling[1] = 1; + } + } + private static bool IsBiColorCompression(TiffCompression? compression) { if (compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or From eda85d20eef0d11391ebe50e5405fc57621b3f1c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 21 May 2024 21:36:59 +1000 Subject: [PATCH 03/43] Define new joining types and extend existing pixel info --- .../Formats/AnimatedImageFrameMetadata.cs | 62 ----- .../Formats/FormatConnectingFrameMetadata.cs | 35 +++ .../Formats/FormatConnectingMetadata.cs | 65 +++++ src/ImageSharp/Formats/FrameBlendMode.cs | 23 ++ src/ImageSharp/Formats/FrameColorTableMode.cs | 20 ++ src/ImageSharp/Formats/FrameDisposalMode.cs | 38 +++ src/ImageSharp/Formats/Gif/GifMetadata.cs | 2 +- .../Formats/IFormatFrameMetadata.cs | 33 +++ src/ImageSharp/Formats/IFormatMetadata.cs | 33 +++ .../Jpeg/Components/Encoder/HuffmanSpec.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 18 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 4 +- src/ImageSharp/PixelFormats/PixelColorType.cs | 62 ++++- .../PixelFormats/PixelComponentInfo.cs | 4 +- .../PixelFormats/PixelColorTypeTests.cs | 255 ++++++++++++++++++ 15 files changed, 573 insertions(+), 83 deletions(-) create mode 100644 src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/FormatConnectingMetadata.cs create mode 100644 src/ImageSharp/Formats/FrameBlendMode.cs create mode 100644 src/ImageSharp/Formats/FrameColorTableMode.cs create mode 100644 src/ImageSharp/Formats/FrameDisposalMode.cs create mode 100644 src/ImageSharp/Formats/IFormatFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/IFormatMetadata.cs create mode 100644 tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs index 75595e1f7d..8f8e187403 100644 --- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs +++ b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs @@ -30,65 +30,3 @@ internal class AnimatedImageFrameMetadata /// public FrameDisposalMode DisposalMode { get; set; } } - -#pragma warning disable SA1201 // Elements should appear in the correct order -internal enum FrameBlendMode -#pragma warning restore SA1201 // Elements should appear in the correct order -{ - /// - /// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame. - /// - Source = 0, - - /// - /// Blend the current frame with the previous frame in the animation sequence within the rectangle covered - /// by the current frame. - /// If the current has any transparent areas, the corresponding areas of the previous frame will be visible - /// through these transparent regions. - /// - Over = 1 -} - -internal enum FrameDisposalMode -{ - /// - /// No disposal specified. - /// The decoder is not required to take any action. - /// - Unspecified = 0, - - /// - /// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to - /// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains - /// transparency, the previous frame will be visible through these transparent areas. - /// - DoNotDispose = 1, - - /// - /// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is - /// filled with the background color specified in the image metadata. - /// This effectively erases the current frame by replacing it with the background color before the next frame is displayed. - /// - RestoreToBackground = 2, - - /// - /// Restore to previous. This method restores the area affected by the current frame to what it was before the - /// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image - /// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small - /// part of the image changes from frame to frame. - /// - RestoreToPrevious = 3 -} - -internal enum FrameColorTableMode -{ - /// - /// The frame uses the shared color table specified by the image metadata. - /// - Global, - - /// - /// The frame uses a color table specified by the frame metadata. - /// - Local -} diff --git a/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs new file mode 100644 index 0000000000..31555afe34 --- /dev/null +++ b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// A metadata format designed to allow conversion between different image format frames. +/// +public class FormatConnectingFrameMetadata +{ + /// + /// Gets or sets the frame color table. + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + /// Gets or sets the frame color table mode. + /// + public FrameColorTableMode ColorTableMode { get; set; } + + /// + /// Gets or sets the duration of the frame. + /// + public TimeSpan Duration { get; set; } + + /// + /// Gets or sets the frame alpha blending mode. + /// + public FrameBlendMode BlendMode { get; set; } + + /// + /// Gets or sets the frame disposal mode. + /// + public FrameDisposalMode DisposalMode { get; set; } +} diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs new file mode 100644 index 0000000000..a7030b4642 --- /dev/null +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// A metadata format designed to allow conversion between different image formats. +/// +public class FormatConnectingMetadata +{ + /// + /// Gets the quality. + /// + /// + /// The value is usually between 1 and 100. Defaults to 100. + /// + public int Quality { get; init; } = 100; + + /// + /// Gets information about the encoded pixel type. + /// + public PixelTypeInfo PixelTypeInfo { get; init; } + + /// + /// Gets the shared color table. + /// + public ReadOnlyMemory? ColorTable { get; init; } + + /// + /// Gets the shared color table mode. + /// + /// + /// Defaults to . + /// + public FrameColorTableMode ColorTableMode { get; init; } + + /// + /// Gets the default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the disposal mode is . + /// + /// + /// Defaults to . + /// + public Color BackgroundColor { get; init; } = Color.Transparent; + + /// + /// Gets the number of times any animation is repeated. + /// + /// + /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1. + /// + public ushort RepeatCount { get; init; } = 1; + + /// + /// Gets a value indicating whether the root frame is shown as part of the animated sequence. + /// + /// + /// Defaults to . + /// + public bool AnimateRootFrame { get; init; } = true; +} diff --git a/src/ImageSharp/Formats/FrameBlendMode.cs b/src/ImageSharp/Formats/FrameBlendMode.cs new file mode 100644 index 0000000000..5785324e5a --- /dev/null +++ b/src/ImageSharp/Formats/FrameBlendMode.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Provides a way to specify how the current frame should be blended with the previous frame in the animation sequence. +/// +public enum FrameBlendMode +{ + /// + /// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame. + /// + Source = 0, + + /// + /// Blend the current frame with the previous frame in the animation sequence within the rectangle covered + /// by the current frame. + /// If the current has any transparent areas, the corresponding areas of the previous frame will be visible + /// through these transparent regions. + /// + Over = 1 +} diff --git a/src/ImageSharp/Formats/FrameColorTableMode.cs b/src/ImageSharp/Formats/FrameColorTableMode.cs new file mode 100644 index 0000000000..a45b6de0e4 --- /dev/null +++ b/src/ImageSharp/Formats/FrameColorTableMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Provides a way to specify how the color table is used by the frame. +/// +public enum FrameColorTableMode +{ + /// + /// The frame uses the shared color table specified by the image metadata. + /// + Global, + + /// + /// The frame uses a color table specified by the frame metadata. + /// + Local +} diff --git a/src/ImageSharp/Formats/FrameDisposalMode.cs b/src/ImageSharp/Formats/FrameDisposalMode.cs new file mode 100644 index 0000000000..196fdb5ad4 --- /dev/null +++ b/src/ImageSharp/Formats/FrameDisposalMode.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Provides a way to specify how the current frame should be disposed of before rendering the next frame. +/// +public enum FrameDisposalMode +{ + /// + /// No disposal specified. + /// The decoder is not required to take any action. + /// + Unspecified = 0, + + /// + /// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to + /// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains + /// transparency, the previous frame will be visible through these transparent areas. + /// + DoNotDispose = 1, + + /// + /// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is + /// filled with the background color specified in the image metadata. + /// This effectively erases the current frame by replacing it with the background color before the next frame is displayed. + /// + RestoreToBackground = 2, + + /// + /// Restore to previous. This method restores the area affected by the current frame to what it was before the + /// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image + /// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small + /// part of the image changes from frame to frame. + /// + RestoreToPrevious = 3 +} diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 1331edee89..7c83de9305 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -67,7 +67,7 @@ public class GifMetadata : IDeepCloneable /// Gets or sets the collection of comments about the graphics, credits, descriptions or any /// other type of non-control and non-graphic data. /// - public IList Comments { get; set; } = new List(); + public IList Comments { get; set; } = []; /// public IDeepCloneable DeepClone() => new GifMetadata(this); diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs new file mode 100644 index 0000000000..bd7c6c45b1 --- /dev/null +++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// An interface that provides metadata for a specific image format frames. +/// +public interface IFormatFrameMetadata : IDeepCloneable +{ + /// + /// Converts the metadata to a instance. + /// + /// The . + FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); +} + +/// +/// An interface that provides metadata for a specific image format frames. +/// +/// The metadata type implementing this interface. +public interface IFormatFrameMetadata : IFormatMetadata, IDeepCloneable + where TSelf : class, IFormatFrameMetadata, new() +{ + /// + /// Creates a new instance of the class from the given . + /// + /// The . + /// The . +#pragma warning disable CA1000 // Do not declare static members on generic types + static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata); +#pragma warning restore CA1000 // Do not declare static members on generic types +} diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs new file mode 100644 index 0000000000..f542f92db3 --- /dev/null +++ b/src/ImageSharp/Formats/IFormatMetadata.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// An interface that provides metadata for a specific image format. +/// +public interface IFormatMetadata : IDeepCloneable +{ + /// + /// Converts the metadata to a instance. + /// + /// The . + FormatConnectingMetadata ToFormatConnectingMetadata(); +} + +/// +/// An interface that provides metadata for a specific image format. +/// +/// The metadata type implementing this interface. +public interface IFormatMetadata : IFormatMetadata, IDeepCloneable + where TSelf : class, IFormatMetadata, new() +{ + /// + /// Creates a new instance of the class from the given . + /// + /// The . + /// The . +#pragma warning disable CA1000 // Do not declare static members on generic types + static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata); +#pragma warning restore CA1000 // Do not declare static members on generic types +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs index faba3d5afc..f5666f7799 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; /// /// The Huffman encoding specifications. /// -public readonly struct HuffmanSpec +internal readonly struct HuffmanSpec { /// /// Huffman talbe specification for luminance DC. diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index fe1324a862..ce4c0758fd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -13,10 +13,7 @@ public class JpegMetadata : IDeepCloneable /// /// Initializes a new instance of the class. /// - public JpegMetadata() - { - this.Comments = new List(); - } + public JpegMetadata() => this.Comments = new List(); /// /// Initializes a new instance of the class. @@ -36,7 +33,7 @@ public class JpegMetadata : IDeepCloneable /// /// /// This value might not be accurate if it was calculated during jpeg decoding - /// with non-complient ITU quantization tables. + /// with non-compliant ITU quantization tables. /// internal int? LuminanceQuality { get; set; } @@ -45,7 +42,7 @@ public class JpegMetadata : IDeepCloneable /// /// /// This value might not be accurate if it was calculated during jpeg decoding - /// with non-complient ITU quantization tables. + /// with non-compliant ITU quantization tables. /// internal int? ChrominanceQuality { get; set; } @@ -69,15 +66,8 @@ public class JpegMetadata : IDeepCloneable return this.LuminanceQuality.Value; } - else - { - if (this.ChrominanceQuality.HasValue) - { - return this.ChrominanceQuality.Value; - } - return Quantization.DefaultQualityFactor; - } + return this.ChrominanceQuality ?? Quantization.DefaultQualityFactor; } } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index d9028dd807..de02e390f0 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -77,7 +77,7 @@ public class PngMetadata : IDeepCloneable /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. /// Used for conveying textual information associated with the image. /// - public IList TextData { get; set; } = new List(); + public IList TextData { get; set; } = []; /// /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. @@ -96,7 +96,7 @@ public class PngMetadata : IDeepCloneable { // Should the conversion be from a format that uses a 24bit palette entries (gif) // we need to clone and adjust the color table to allow for transparency. - Color[]? colorTable = metadata.ColorTable.HasValue ? metadata.ColorTable.Value.ToArray() : null; + Color[]? colorTable = metadata.ColorTable?.ToArray(); if (colorTable != null) { for (int i = 0; i < colorTable.Length; i++) diff --git a/src/ImageSharp/PixelFormats/PixelColorType.cs b/src/ImageSharp/PixelFormats/PixelColorType.cs index 9ac2c308c2..86c3d51e97 100644 --- a/src/ImageSharp/PixelFormats/PixelColorType.cs +++ b/src/ImageSharp/PixelFormats/PixelColorType.cs @@ -9,6 +9,11 @@ namespace SixLabors.ImageSharp.PixelFormats; [Flags] public enum PixelColorType { + /// + /// No color type. + /// + None = 0, + /// /// Represents the Red component of the color. /// @@ -42,5 +47,60 @@ public enum PixelColorType /// /// Indicates that the color is in BGR (Blue, Green, Red) format. /// - BGR = Blue | Green | Red | (1 << 6) + BGR = Blue | Green | Red | (1 << 6), + + /// + /// Represents the Luminance component in YCbCr. + /// + Luminance = 1 << 7, + + /// + /// Represents the Chrominance Blue component in YCbCr. + /// + ChrominanceBlue = 1 << 8, + + /// + /// Represents the Chrominance Red component in YCbCr. + /// + ChrominanceRed = 1 << 9, + + /// + /// Indicates that the color is in YCbCr (Luminance, Chrominance Blue, Chrominance Red) format. + /// + YCbCr = Luminance | ChrominanceBlue | ChrominanceRed, + + /// + /// Represents the Cyan component in CMYK. + /// + Cyan = 1 << 10, + + /// + /// Represents the Magenta component in CMYK. + /// + Magenta = 1 << 11, + + /// + /// Represents the Yellow component in CMYK. + /// + Yellow = 1 << 12, + + /// + /// Represents the Key (black) component in CMYK and YCCK. + /// + Key = 1 << 13, + + /// + /// Indicates that the color is in CMYK (Cyan, Magenta, Yellow, Key) format. + /// + CMYK = Cyan | Magenta | Yellow | Key, + + /// + /// Indicates that the color is in YCCK (Luminance, Chrominance Blue, Chrominance Red, Key) format. + /// + YCCK = Luminance | ChrominanceBlue | ChrominanceRed | Key, + + /// + /// Indicates that the color is of a type not specified in this enum. + /// + Other = 1 << 14 } diff --git a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs index a761054144..fca371ca92 100644 --- a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs +++ b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs @@ -44,7 +44,7 @@ public readonly struct PixelComponentInfo { if (precision.Length != count || precision.Length > 16) { - throw new ArgumentException($"Count must match the length of precision array and cannot exceed 16."); + throw new ArgumentOutOfRangeException(nameof(count), $"Count {count} must match the length of precision array and cannot exceed 16."); } long precisionData1 = 0; @@ -55,7 +55,7 @@ public readonly struct PixelComponentInfo int p = precision[i]; if (p is < 0 or > 255) { - throw new ArgumentException("Precision must be between 0 and 255."); + throw new ArgumentOutOfRangeException(nameof(precision), $"Precision {precision.Length} must be between 0 and 255."); } if (i < 8) diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs new file mode 100644 index 0000000000..215fe7b876 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs @@ -0,0 +1,255 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +public class PixelColorTypeTests +{ + [Fact] + public void PixelColorType_RedFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Red; + Assert.True(colorType.HasFlag(PixelColorType.Red)); + } + + [Fact] + public void PixelColorType_GreenFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Green; + Assert.True(colorType.HasFlag(PixelColorType.Green)); + } + + [Fact] + public void PixelColorType_BlueFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Blue; + Assert.True(colorType.HasFlag(PixelColorType.Blue)); + } + + [Fact] + public void PixelColorType_AlphaFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Alpha; + Assert.True(colorType.HasFlag(PixelColorType.Alpha)); + } + + [Fact] + public void PixelColorType_GrayscaleFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Grayscale; + Assert.True(colorType.HasFlag(PixelColorType.Grayscale)); + } + + [Fact] + public void PixelColorType_RGBFlags_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.RGB; + Assert.True(colorType.HasFlag(PixelColorType.Red)); + Assert.True(colorType.HasFlag(PixelColorType.Green)); + Assert.True(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.BGR)); + } + + [Fact] + public void PixelColorType_BGRFlags_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.BGR; + Assert.True(colorType.HasFlag(PixelColorType.Blue)); + Assert.True(colorType.HasFlag(PixelColorType.Green)); + Assert.True(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.RGB)); + } + + [Fact] + public void PixelColorType_LuminanceFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Luminance; + Assert.True(colorType.HasFlag(PixelColorType.Luminance)); + } + + [Fact] + public void PixelColorType_ChrominanceBlueFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.ChrominanceBlue; + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + } + + [Fact] + public void PixelColorType_ChrominanceRedFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.ChrominanceRed; + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceRed)); + } + + [Fact] + public void PixelColorType_YCbCrFlags_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.YCbCr; + Assert.True(colorType.HasFlag(PixelColorType.Luminance)); + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceRed)); + } + + [Fact] + public void PixelColorType_CyanFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Cyan; + Assert.True(colorType.HasFlag(PixelColorType.Cyan)); + } + + [Fact] + public void PixelColorType_MagentaFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Magenta; + Assert.True(colorType.HasFlag(PixelColorType.Magenta)); + } + + [Fact] + public void PixelColorType_YellowFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Yellow; + Assert.True(colorType.HasFlag(PixelColorType.Yellow)); + } + + [Fact] + public void PixelColorType_KeyFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Key; + Assert.True(colorType.HasFlag(PixelColorType.Key)); + } + + [Fact] + public void PixelColorType_CMYKFlags_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.CMYK; + Assert.True(colorType.HasFlag(PixelColorType.Cyan)); + Assert.True(colorType.HasFlag(PixelColorType.Magenta)); + Assert.True(colorType.HasFlag(PixelColorType.Yellow)); + Assert.True(colorType.HasFlag(PixelColorType.Key)); + } + + [Fact] + public void PixelColorType_YCCKFlags_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.YCCK; + Assert.True(colorType.HasFlag(PixelColorType.Luminance)); + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.True(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.True(colorType.HasFlag(PixelColorType.Key)); + } + + [Fact] + public void PixelColorType_Other_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Other; + Assert.True(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_None_ShouldBeZero() + { + const PixelColorType colorType = PixelColorType.None; + Assert.Equal(0, (int)colorType); + } + + [Fact] + public void PixelColorType_RGB_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.RGB; + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_BGR_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.BGR; + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_YCbCr_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.YCbCr; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_CMYK_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.CMYK; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_YCCK_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.YCCK; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_Other_ShouldNotContainPreviousFlags() + { + const PixelColorType colorType = PixelColorType.Other; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.RGB)); + Assert.False(colorType.HasFlag(PixelColorType.BGR)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.YCbCr)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.CMYK)); + Assert.False(colorType.HasFlag(PixelColorType.YCCK)); + } +} From a971082a0c02001e4b5fc94cad1d9d31a6a51371 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 21 May 2024 21:43:19 +1000 Subject: [PATCH 04/43] Sketch out Bmp implementation --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 25 ++++++++++++++++--- .../Formats/IFormatFrameMetadata.cs | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index a2ed1d21d0..f381249d8f 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; @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// Provides Bmp specific metadata information for the image. /// -public class BmpMetadata : IDeepCloneable +public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -36,7 +36,26 @@ public class BmpMetadata : IDeepCloneable public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; /// - public IDeepCloneable DeepClone() => new BmpMetadata(this); + public static BmpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + => throw new NotImplementedException(); + + /// + public static BmpMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => throw new NotImplementedException(); + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => throw new NotImplementedException(); + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => throw new NotImplementedException(); + + /// + public IDeepCloneable DeepClone() => ((IDeepCloneable)this).DeepClone(); + + /// + BmpMetadata IDeepCloneable.DeepClone() => new(this); // TODO: Colors used once we support encoding palette bmps. } diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs index bd7c6c45b1..4c5321073d 100644 --- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs +++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -19,7 +19,7 @@ public interface IFormatFrameMetadata : IDeepCloneable /// An interface that provides metadata for a specific image format frames. /// /// The metadata type implementing this interface. -public interface IFormatFrameMetadata : IFormatMetadata, IDeepCloneable +public interface IFormatFrameMetadata : IFormatFrameMetadata, IDeepCloneable where TSelf : class, IFormatFrameMetadata, new() { /// From d52815bfd0730ffb4dd1aea7acc12e21a8bc8092 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 21 May 2024 23:05:49 +1000 Subject: [PATCH 05/43] Wire up BmpMetadata proper --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 96 +++++++++++++++++-- .../Formats/FormatConnectingMetadata.cs | 2 +- src/ImageSharp/PixelFormats/PixelColorType.cs | 7 +- .../PixelFormats/PixelComponentInfo.cs | 13 ++- .../PixelFormats/PixelColorTypeTests.cs | 37 +++++++ 5 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index f381249d8f..2f03b9c6f3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + +// TODO: Add color table information. namespace SixLabors.ImageSharp.Formats.Bmp; /// @@ -37,25 +40,106 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata public static BmpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) - => throw new NotImplementedException(); + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + if (bpp == 1) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel1 }; + } + + if (bpp == 2) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel2 }; + } + + if (bpp <= 4) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel4 }; + } + + if (bpp <= 8) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 }; + } + + if (bpp <= 16) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel16, InfoHeaderType = BmpInfoHeaderType.WinVersion3 }; + } + + if (bpp <= 24) + { + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.WinVersion4 }; + } + + return new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel32, InfoHeaderType = BmpInfoHeaderType.WinVersion5 }; + } /// public static BmpMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) - => throw new NotImplementedException(); + => new(); /// public FormatConnectingMetadata ToFormatConnectingMetadata() - => throw new NotImplementedException(); + { + int bpp = (int)this.BitsPerPixel; + + PixelAlphaRepresentation alpha = this.InfoHeaderType switch + { + BmpInfoHeaderType.WinVersion2 or + BmpInfoHeaderType.Os2Version2Short or + BmpInfoHeaderType.WinVersion3 or + BmpInfoHeaderType.AdobeVersion3 or + BmpInfoHeaderType.Os2Version2 => PixelAlphaRepresentation.None, + BmpInfoHeaderType.AdobeVersion3WithAlpha or + BmpInfoHeaderType.WinVersion4 or + BmpInfoHeaderType.WinVersion5 or + _ => bpp < 32 ? PixelAlphaRepresentation.None : PixelAlphaRepresentation.Unassociated + }; + + PixelComponentInfo info = this.BitsPerPixel switch + { + BmpBitsPerPixel.Pixel1 => PixelComponentInfo.Create(1, bpp, 1), + BmpBitsPerPixel.Pixel2 => PixelComponentInfo.Create(1, bpp, 2), + BmpBitsPerPixel.Pixel4 => PixelComponentInfo.Create(1, bpp, 4), + BmpBitsPerPixel.Pixel8 => PixelComponentInfo.Create(1, bpp, 8), + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + BmpBitsPerPixel.Pixel16 => PixelComponentInfo.Create(3, bpp, 5, 6, 5), + BmpBitsPerPixel.Pixel24 => PixelComponentInfo.Create(3, bpp, 8, 8, 8), + BmpBitsPerPixel.Pixel32 or _ => PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8), + }; + + PixelColorType color = this.BitsPerPixel switch + { + BmpBitsPerPixel.Pixel1 or + BmpBitsPerPixel.Pixel2 or + BmpBitsPerPixel.Pixel4 or + BmpBitsPerPixel.Pixel8 => PixelColorType.Indexed, + BmpBitsPerPixel.Pixel16 or + BmpBitsPerPixel.Pixel24 => PixelColorType.RGB, + BmpBitsPerPixel.Pixel32 or _ => PixelColorType.RGB | PixelColorType.Alpha, + }; + + return new() + { + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + } + }; + } /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => throw new NotImplementedException(); + => new(); /// public IDeepCloneable DeepClone() => ((IDeepCloneable)this).DeepClone(); /// BmpMetadata IDeepCloneable.DeepClone() => new(this); - - // TODO: Colors used once we support encoding palette bmps. } diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index a7030b4642..2acbe96990 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -34,7 +34,7 @@ public class FormatConnectingMetadata /// /// Defaults to . /// - public FrameColorTableMode ColorTableMode { get; init; } + public FrameColorTableMode ColorTableMode { get; init; } = FrameColorTableMode.Global; /// /// Gets the default background color of the canvas when animating. diff --git a/src/ImageSharp/PixelFormats/PixelColorType.cs b/src/ImageSharp/PixelFormats/PixelColorType.cs index 86c3d51e97..52d4df73a0 100644 --- a/src/ImageSharp/PixelFormats/PixelColorType.cs +++ b/src/ImageSharp/PixelFormats/PixelColorType.cs @@ -99,8 +99,13 @@ public enum PixelColorType /// YCCK = Luminance | ChrominanceBlue | ChrominanceRed | Key, + /// + /// Indicates that the color is indexed using a palette. + /// + Indexed = 1 << 14, + /// /// Indicates that the color is of a type not specified in this enum. /// - Other = 1 << 14 + Other = 1 << 15 } diff --git a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs index fca371ca92..64b233f357 100644 --- a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs +++ b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs @@ -41,6 +41,17 @@ public readonly struct PixelComponentInfo /// The component precision and index cannot exceed the component range. public static PixelComponentInfo Create(int count, params int[] precision) where TPixel : unmanaged, IPixel + => Create(count, Unsafe.SizeOf() * 8, precision); + + /// + /// Creates a new instance. + /// + /// The number of components within the pixel format. + /// The number of bits per pixel. + /// The precision in bits of each component. + /// The . + /// The component precision and index cannot exceed the component range. + public static PixelComponentInfo Create(int count, int bitsPerPixel, params int[] precision) { if (precision.Length != count || precision.Length > 16) { @@ -70,7 +81,7 @@ public readonly struct PixelComponentInfo sum += p; } - return new PixelComponentInfo(count, (Unsafe.SizeOf() * 8) - sum, precisionData1, precisionData2); + return new PixelComponentInfo(count, bitsPerPixel - sum, precisionData1, precisionData2); } /// diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs index 215fe7b876..09e84d53be 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs @@ -140,6 +140,13 @@ public class PixelColorTypeTests Assert.True(colorType.HasFlag(PixelColorType.Key)); } + [Fact] + public void PixelColorType_Indexed_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Indexed; + Assert.True(colorType.HasFlag(PixelColorType.Indexed)); + } + [Fact] public void PixelColorType_Other_ShouldBeSet() { @@ -166,6 +173,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -181,6 +189,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -197,6 +206,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -212,6 +222,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Luminance)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -227,6 +238,31 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + + [Fact] + public void PixelColorType_Indexed_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.Indexed; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.RGB)); + Assert.False(colorType.HasFlag(PixelColorType.BGR)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.YCbCr)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.CMYK)); + Assert.False(colorType.HasFlag(PixelColorType.YCCK)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -251,5 +287,6 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Key)); Assert.False(colorType.HasFlag(PixelColorType.CMYK)); Assert.False(colorType.HasFlag(PixelColorType.YCCK)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); } } From 2752a450d8be7856a0a0d1528d675752cff76d56 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 May 2024 14:05:14 +1000 Subject: [PATCH 06/43] Implement up GifMetadata --- .../Formats/Gif/GifColorTableMode.cs | 20 ------ src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 8 +-- src/ImageSharp/Formats/Gif/GifEncoder.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 12 ++-- .../Formats/Gif/GifFrameMetadata.cs | 4 +- src/ImageSharp/Formats/Gif/GifMetadata.cs | 64 ++++++++++++++++-- .../Formats/Gif/MetadataExtensions.cs | 6 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 +- src/ImageSharp/PixelFormats/PixelColorType.cs | 40 +++++------ .../PixelFormats/PixelComponentInfo.cs | 2 +- .../PixelFormats/PixelImplementations/L16.cs | 2 +- .../PixelFormats/PixelImplementations/L8.cs | 2 +- .../PixelFormats/PixelImplementations/La16.cs | 2 +- .../PixelFormats/PixelImplementations/La32.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 15 +++-- .../Formats/Gif/GifMetadataTests.cs | 10 +-- .../ImageFrameCollectionTests.NonGeneric.cs | 3 +- .../ImageSharp.Tests/PixelFormats/L16Tests.cs | 2 +- .../ImageSharp.Tests/PixelFormats/L8Tests.cs | 2 +- .../PixelFormats/La16Tests.cs | 2 +- .../PixelFormats/La32Tests.cs | 2 +- .../PixelFormats/PixelColorTypeTests.cs | 66 ++++++++++--------- 22 files changed, 154 insertions(+), 118 deletions(-) delete mode 100644 src/ImageSharp/Formats/Gif/GifColorTableMode.cs diff --git a/src/ImageSharp/Formats/Gif/GifColorTableMode.cs b/src/ImageSharp/Formats/Gif/GifColorTableMode.cs deleted file mode 100644 index 0f65f46022..0000000000 --- a/src/ImageSharp/Formats/Gif/GifColorTableMode.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Gif; - -/// -/// Provides enumeration for the available color table modes. -/// -public enum GifColorTableMode -{ - /// - /// A single color table is calculated from the first frame and reused for subsequent frames. - /// - Global, - - /// - /// A unique color table is calculated for each frame. - /// - Local -} diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index d64792eba7..a67ee8f982 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -696,14 +696,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals && this.logicalScreenDescriptor.GlobalColorTableSize > 0) { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); - gifMeta.ColorTableMode = GifColorTableMode.Global; + gifMeta.ColorTableMode = FrameColorTableMode.Global; } if (this.imageDescriptor.LocalColorTableFlag && this.imageDescriptor.LocalColorTableSize > 0) { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); - gifMeta.ColorTableMode = GifColorTableMode.Local; + gifMeta.ColorTableMode = FrameColorTableMode.Local; Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize]; ReadOnlySpan rgbTable = MemoryMarshal.Cast(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]); @@ -766,8 +766,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals this.metadata = meta; this.gifMetadata = meta.GetGifMetadata(); this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag - ? GifColorTableMode.Global - : GifColorTableMode.Local; + ? FrameColorTableMode.Global + : FrameColorTableMode.Local; if (this.logicalScreenDescriptor.GlobalColorTableFlag) { diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index ab05548ac5..6cb8f9d8ce 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -11,7 +11,7 @@ public sealed class GifEncoder : QuantizingImageEncoder /// /// Gets the color table mode: Global or local. /// - public GifColorTableMode? ColorTableMode { get; init; } + public FrameColorTableMode? ColorTableMode { get; init; } /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 95429e6065..70afe12e10 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -49,7 +49,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// The color table mode: Global or local. /// - private GifColorTableMode? colorTableMode; + private FrameColorTableMode? colorTableMode; /// /// The pixel sampling strategy for global quantization. @@ -87,7 +87,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals GifMetadata gifMetadata = GetGifMetadata(image); this.colorTableMode ??= gifMetadata.ColorTableMode; - bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; + bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global; // Quantize the first image frame returning a palette. IndexedImageFrame? quantized = null; @@ -99,7 +99,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals if (this.quantizer is null) { // Is this a gif with color information. If so use that, otherwise use octree. - if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) + if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) { // We avoid dithering by default to preserve the original colors. int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); @@ -221,7 +221,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals metadata = GifFrameMetadata.FromAnimatedMetadata(ani); } - if (metadata?.ColorTableMode == GifColorTableMode.Global && transparencyIndex > -1) + if (metadata?.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1) { metadata.HasTransparency = true; metadata.TransparencyIndex = ClampIndex(transparencyIndex); @@ -258,7 +258,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); - bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local); + bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); if (!useLocal && !hasPaletteQuantizer && i > 0) { @@ -301,7 +301,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; Rectangle interest = indices.FullRectangle(); - bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata.ColorTableMode == GifColorTableMode.Local); + bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); this.WriteImageDescriptor(interest, useLocal, bitDepth, stream); diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index 6598def2a5..3f8563706d 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -40,7 +40,7 @@ public class GifFrameMetadata : IDeepCloneable /// /// Gets or sets the color table mode. /// - public GifColorTableMode ColorTableMode { get; set; } + public FrameColorTableMode ColorTableMode { get; set; } /// /// Gets or sets the local color table, if any. @@ -101,7 +101,7 @@ public class GifFrameMetadata : IDeepCloneable return new() { LocalColorTable = metadata.ColorTable, - ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, + ColorTableMode = metadata.ColorTableMode, FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), DisposalMethod = GetMode(metadata.DisposalMode), HasTransparency = hasTransparency, diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 7c83de9305..43935504b1 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Provides Gif specific metadata information for the image. /// -public class GifMetadata : IDeepCloneable +public class GifMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -49,7 +49,7 @@ public class GifMetadata : IDeepCloneable /// /// Gets or sets the color table mode. /// - public GifColorTableMode ColorTableMode { get; set; } + public FrameColorTableMode ColorTableMode { get; set; } /// /// Gets or sets the global color table, if any. @@ -69,9 +69,6 @@ public class GifMetadata : IDeepCloneable /// public IList Comments { get; set; } = []; - /// - public IDeepCloneable DeepClone() => new GifMetadata(this); - internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) { int index = 0; @@ -92,9 +89,64 @@ public class GifMetadata : IDeepCloneable return new() { GlobalColorTable = metadata.ColorTable, - ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, + ColorTableMode = metadata.ColorTableMode, + RepeatCount = metadata.RepeatCount, + BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255), + }; + } + + /// + public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int index = 0; + Color background = metadata.BackgroundColor; + if (metadata.ColorTable.HasValue) + { + ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; + for (int i = 0; i < colorTable.Length; i++) + { + if (background == colorTable[i]) + { + index = i; + break; + } + } + } + + return new() + { + GlobalColorTable = metadata.ColorTable, + ColorTableMode = metadata.ColorTableMode, RepeatCount = metadata.RepeatCount, BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255), }; } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + Color color = this.GlobalColorTable.HasValue && this.GlobalColorTable.Value.Span.Length > this.BackgroundColorIndex + ? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex] + : Color.Transparent; + + return new() + { + AnimateRootFrame = true, + BackgroundColor = color, + ColorTable = this.GlobalColorTable, + ColorTableMode = this.ColorTableMode, + PixelTypeInfo = new PixelTypeInfo(24) + { + ColorType = PixelColorType.Indexed, + ComponentInfo = PixelComponentInfo.Create(3, 24, 8, 8, 8), + }, + RepeatCount = this.RepeatCount, + }; + } + + /// + public IDeepCloneable DeepClone() => ((IDeepCloneable)this).DeepClone(); + + /// + GifMetadata IDeepCloneable.DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index ad06462e77..d4650403cb 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -70,7 +70,7 @@ public static partial class MetadataExtensions return new() { ColorTable = source.GlobalColorTable, - ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, + ColorTableMode = source.ColorTableMode, RepeatCount = source.RepeatCount, BackgroundColor = background, }; @@ -83,12 +83,12 @@ public static partial class MetadataExtensions bool blendSource = source.DisposalMethod == GifDisposalMethod.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); // If the color table is global and frame has no transparency. Consider it 'Source' also. - blendSource |= source.ColorTableMode == GifColorTableMode.Global && !source.HasTransparency; + blendSource |= source.ColorTableMode == FrameColorTableMode.Global && !source.HasTransparency; return new() { ColorTable = source.LocalColorTable, - ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, + ColorTableMode = source.ColorTableMode, Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), DisposalMode = GetMode(source.DisposalMethod), BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 6e8224f01e..36ce136f47 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1680,14 +1680,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { return info.ColorType switch { - PixelColorType.Grayscale => PngColorType.Grayscale, + PixelColorType.Luminance => PngColorType.Grayscale, _ => PngColorType.Rgb, }; } return info.ColorType switch { - PixelColorType.Grayscale | PixelColorType.Alpha or PixelColorType.Alpha => PngColorType.GrayscaleWithAlpha, + PixelColorType.Luminance | PixelColorType.Alpha or PixelColorType.Alpha => PngColorType.GrayscaleWithAlpha, _ => PngColorType.RgbWithAlpha, }; } diff --git a/src/ImageSharp/PixelFormats/PixelColorType.cs b/src/ImageSharp/PixelFormats/PixelColorType.cs index 52d4df73a0..afbe6a4d3e 100644 --- a/src/ImageSharp/PixelFormats/PixelColorType.cs +++ b/src/ImageSharp/PixelFormats/PixelColorType.cs @@ -35,34 +35,39 @@ public enum PixelColorType Alpha = 1 << 3, /// - /// Indicates that the color is in grayscale. + /// Represents the Exponent component used in formats like R9G9B9E5. /// - Grayscale = 1 << 4, + Exponent = 1 << 4, /// - /// Indicates that the color is in RGB (Red, Green, Blue) format. + /// Indicates that the color is in luminance (grayscale) format. /// - RGB = Red | Green | Blue | (1 << 5), + Luminance = 1 << 5, /// - /// Indicates that the color is in BGR (Blue, Green, Red) format. + /// Indicates that the color is indexed using a palette. + /// + Indexed = 1 << 6, + + /// + /// Indicates that the color is in RGB (Red, Green, Blue) format. /// - BGR = Blue | Green | Red | (1 << 6), + RGB = Red | Green | Blue | (1 << 7), /// - /// Represents the Luminance component in YCbCr. + /// Indicates that the color is in BGR (Blue, Green, Red) format. /// - Luminance = 1 << 7, + BGR = Blue | Green | Red | (1 << 8), /// /// Represents the Chrominance Blue component in YCbCr. /// - ChrominanceBlue = 1 << 8, + ChrominanceBlue = 1 << 9, /// /// Represents the Chrominance Red component in YCbCr. /// - ChrominanceRed = 1 << 9, + ChrominanceRed = 1 << 10, /// /// Indicates that the color is in YCbCr (Luminance, Chrominance Blue, Chrominance Red) format. @@ -72,22 +77,22 @@ public enum PixelColorType /// /// Represents the Cyan component in CMYK. /// - Cyan = 1 << 10, + Cyan = 1 << 11, /// /// Represents the Magenta component in CMYK. /// - Magenta = 1 << 11, + Magenta = 1 << 12, /// /// Represents the Yellow component in CMYK. /// - Yellow = 1 << 12, + Yellow = 1 << 13, /// /// Represents the Key (black) component in CMYK and YCCK. /// - Key = 1 << 13, + Key = 1 << 14, /// /// Indicates that the color is in CMYK (Cyan, Magenta, Yellow, Key) format. @@ -99,13 +104,8 @@ public enum PixelColorType /// YCCK = Luminance | ChrominanceBlue | ChrominanceRed | Key, - /// - /// Indicates that the color is indexed using a palette. - /// - Indexed = 1 << 14, - /// /// Indicates that the color is of a type not specified in this enum. /// - Other = 1 << 15 + Other = 1 << 16 } diff --git a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs index 64b233f357..1444b344b6 100644 --- a/src/ImageSharp/PixelFormats/PixelComponentInfo.cs +++ b/src/ImageSharp/PixelFormats/PixelComponentInfo.cs @@ -53,7 +53,7 @@ public readonly struct PixelComponentInfo /// The component precision and index cannot exceed the component range. public static PixelComponentInfo Create(int count, int bitsPerPixel, params int[] precision) { - if (precision.Length != count || precision.Length > 16) + if (precision.Length < count || precision.Length > 16) { throw new ArgumentOutOfRangeException(nameof(count), $"Count {count} must match the length of precision array and cannot exceed 16."); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs b/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs index 2b5241b0bc..64a22060c0 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs @@ -71,7 +71,7 @@ public partial struct L16 : IPixel, IPackedVector public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( PixelComponentInfo.Create(1, 16), - PixelColorType.Grayscale, + PixelColorType.Luminance, PixelAlphaRepresentation.None); /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs b/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs index 5d733bdbb3..cf8646cfa0 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs @@ -73,7 +73,7 @@ public partial struct L8 : IPixel, IPackedVector public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( PixelComponentInfo.Create(1, 8), - PixelColorType.Grayscale, + PixelColorType.Luminance, PixelAlphaRepresentation.None); /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs b/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs index 69ca662187..026d0c299d 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs @@ -100,7 +100,7 @@ public partial struct La16 : IPixel, IPackedVector public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( PixelComponentInfo.Create(2, 8, 8), - PixelColorType.Grayscale | PixelColorType.Alpha, + PixelColorType.Luminance | PixelColorType.Alpha, PixelAlphaRepresentation.Unassociated); /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs index 1886ef39a1..0ddcf16a1f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs @@ -97,7 +97,7 @@ public partial struct La32 : IPixel, IPackedVector public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( PixelComponentInfo.Create(2, 16, 16), - PixelColorType.Grayscale | PixelColorType.Alpha, + PixelColorType.Luminance | PixelColorType.Alpha, PixelAlphaRepresentation.Unassociated); /// diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index a7e16f7737..767141f56a 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; @@ -113,7 +114,7 @@ public class GifEncoderTests using Image image = provider.GetImage(); GifEncoder encoder = new() { - ColorTableMode = GifColorTableMode.Global, + ColorTableMode = FrameColorTableMode.Global, Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; @@ -122,7 +123,7 @@ public class GifEncoderTests encoder = new() { - ColorTableMode = GifColorTableMode.Local, + ColorTableMode = FrameColorTableMode.Local, Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }), }; @@ -148,7 +149,7 @@ public class GifEncoderTests GifEncoder encoder = new() { - ColorTableMode = GifColorTableMode.Global, + ColorTableMode = FrameColorTableMode.Global, PixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) }; @@ -175,10 +176,10 @@ public class GifEncoderTests Image image = Image.Load(inStream); GifMetadata metaData = image.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); - GifColorTableMode colorMode = metaData.ColorTableMode; + FrameColorTableMode colorMode = metaData.ColorTableMode; int maxColors; - if (colorMode == GifColorTableMode.Global) + if (colorMode == FrameColorTableMode.Global) { maxColors = metaData.GlobalColorTable.Value.Length; } @@ -204,7 +205,7 @@ public class GifEncoderTests // Gifiddle and Cyotek GifInfo say this image has 64 colors. colorMode = cloneMetadata.ColorTableMode; - if (colorMode == GifColorTableMode.Global) + if (colorMode == FrameColorTableMode.Global) { maxColors = metaData.GlobalColorTable.Value.Length; } @@ -220,7 +221,7 @@ public class GifEncoderTests GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata cMeta = clone.Frames[i].Metadata.GetGifMetadata(); - if (iMeta.ColorTableMode == GifColorTableMode.Local) + if (iMeta.ColorTableMode == FrameColorTableMode.Local) { Assert.Equal(iMeta.LocalColorTable.Value.Length, cMeta.LocalColorTable.Value.Length); } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index fb4445cdac..c7babe9322 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -33,7 +33,7 @@ public class GifMetadataTests GifMetadata meta = new() { RepeatCount = 1, - ColorTableMode = GifColorTableMode.Global, + ColorTableMode = FrameColorTableMode.Global, GlobalColorTable = new[] { Color.Black, Color.White }, Comments = new List { "Foo" } }; @@ -41,7 +41,7 @@ public class GifMetadataTests GifMetadata clone = (GifMetadata)meta.DeepClone(); clone.RepeatCount = 2; - clone.ColorTableMode = GifColorTableMode.Local; + clone.ColorTableMode = FrameColorTableMode.Local; clone.GlobalColorTable = new[] { Color.Black }; Assert.False(meta.RepeatCount.Equals(clone.RepeatCount)); @@ -183,11 +183,11 @@ public class GifMetadataTests } [Theory] - [InlineData(TestImages.Gif.Cheers, 93, GifColorTableMode.Global, 256, 4, GifDisposalMethod.NotDispose)] + [InlineData(TestImages.Gif.Cheers, 93, FrameColorTableMode.Global, 256, 4, GifDisposalMethod.NotDispose)] public void Identify_Frames( string imagePath, int framesCount, - GifColorTableMode colorTableMode, + FrameColorTableMode colorTableMode, int globalColorTableLength, int frameDelay, GifDisposalMethod disposalMethod) @@ -206,7 +206,7 @@ public class GifMetadataTests Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode); - if (colorTableMode == GifColorTableMode.Global) + if (colorTableMode == FrameColorTableMode.Global) { Assert.Equal(globalColorTableLength, gifMetadata.GlobalColorTable.Value.Length); } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index a42dcc4812..572c5b2ebd 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.PixelFormats; @@ -315,7 +316,7 @@ public abstract partial class ImageFrameCollectionTests Assert.Equal(aData.DisposalMethod, bData.DisposalMethod); Assert.Equal(aData.FrameDelay, bData.FrameDelay); - if (aData.ColorTableMode == GifColorTableMode.Local && bData.ColorTableMode == GifColorTableMode.Local) + if (aData.ColorTableMode == FrameColorTableMode.Local && bData.ColorTableMode == FrameColorTableMode.Local) { Assert.Equal(aData.LocalColorTable.Value.Length, bData.LocalColorTable.Value.Length); } diff --git a/tests/ImageSharp.Tests/PixelFormats/L16Tests.cs b/tests/ImageSharp.Tests/PixelFormats/L16Tests.cs index 2ddf1accb6..7f0a4217c1 100644 --- a/tests/ImageSharp.Tests/PixelFormats/L16Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/L16Tests.cs @@ -161,7 +161,7 @@ public class L16Tests PixelTypeInfo info = L16.GetPixelTypeInfo(); Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); Assert.Equal(PixelAlphaRepresentation.None, info.AlphaRepresentation); - Assert.Equal(PixelColorType.Grayscale, info.ColorType); + Assert.Equal(PixelColorType.Luminance, info.ColorType); PixelComponentInfo componentInfo = info.ComponentInfo.Value; Assert.Equal(1, componentInfo.ComponentCount); diff --git a/tests/ImageSharp.Tests/PixelFormats/L8Tests.cs b/tests/ImageSharp.Tests/PixelFormats/L8Tests.cs index 40c746cf21..1ca865ef41 100644 --- a/tests/ImageSharp.Tests/PixelFormats/L8Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/L8Tests.cs @@ -245,7 +245,7 @@ public class L8Tests PixelTypeInfo info = L8.GetPixelTypeInfo(); Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); Assert.Equal(PixelAlphaRepresentation.None, info.AlphaRepresentation); - Assert.Equal(PixelColorType.Grayscale, info.ColorType); + Assert.Equal(PixelColorType.Luminance, info.ColorType); PixelComponentInfo componentInfo = info.ComponentInfo.Value; Assert.Equal(1, componentInfo.ComponentCount); diff --git a/tests/ImageSharp.Tests/PixelFormats/La16Tests.cs b/tests/ImageSharp.Tests/PixelFormats/La16Tests.cs index a18b31f6ba..f6cbfc4426 100644 --- a/tests/ImageSharp.Tests/PixelFormats/La16Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/La16Tests.cs @@ -248,7 +248,7 @@ public class La16Tests PixelTypeInfo info = La16.GetPixelTypeInfo(); Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); Assert.Equal(PixelAlphaRepresentation.Unassociated, info.AlphaRepresentation); - Assert.Equal(PixelColorType.Grayscale | PixelColorType.Alpha, info.ColorType); + Assert.Equal(PixelColorType.Luminance | PixelColorType.Alpha, info.ColorType); PixelComponentInfo componentInfo = info.ComponentInfo.Value; Assert.Equal(2, componentInfo.ComponentCount); diff --git a/tests/ImageSharp.Tests/PixelFormats/La32Tests.cs b/tests/ImageSharp.Tests/PixelFormats/La32Tests.cs index 3c702419d3..fd5556d3bc 100644 --- a/tests/ImageSharp.Tests/PixelFormats/La32Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/La32Tests.cs @@ -167,7 +167,7 @@ public class La32Tests PixelTypeInfo info = La32.GetPixelTypeInfo(); Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); Assert.Equal(PixelAlphaRepresentation.Unassociated, info.AlphaRepresentation); - Assert.Equal(PixelColorType.Grayscale | PixelColorType.Alpha, info.ColorType); + Assert.Equal(PixelColorType.Luminance | PixelColorType.Alpha, info.ColorType); PixelComponentInfo componentInfo = info.ComponentInfo.Value; Assert.Equal(2, componentInfo.ComponentCount); diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs index 09e84d53be..c427130054 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs @@ -36,10 +36,24 @@ public class PixelColorTypeTests } [Fact] - public void PixelColorType_GrayscaleFlag_ShouldBeSet() + public void PixelColorType_Exponent_ShouldBeSet() { - const PixelColorType colorType = PixelColorType.Grayscale; - Assert.True(colorType.HasFlag(PixelColorType.Grayscale)); + const PixelColorType colorType = PixelColorType.Exponent; + Assert.True(colorType.HasFlag(PixelColorType.Exponent)); + } + + [Fact] + public void PixelColorType_LuminanceFlag_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Luminance; + Assert.True(colorType.HasFlag(PixelColorType.Luminance)); + } + + [Fact] + public void PixelColorType_Indexed_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Indexed; + Assert.True(colorType.HasFlag(PixelColorType.Indexed)); } [Fact] @@ -62,13 +76,6 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.RGB)); } - [Fact] - public void PixelColorType_LuminanceFlag_ShouldBeSet() - { - const PixelColorType colorType = PixelColorType.Luminance; - Assert.True(colorType.HasFlag(PixelColorType.Luminance)); - } - [Fact] public void PixelColorType_ChrominanceBlueFlag_ShouldBeSet() { @@ -140,13 +147,6 @@ public class PixelColorTypeTests Assert.True(colorType.HasFlag(PixelColorType.Key)); } - [Fact] - public void PixelColorType_Indexed_ShouldBeSet() - { - const PixelColorType colorType = PixelColorType.Indexed; - Assert.True(colorType.HasFlag(PixelColorType.Indexed)); - } - [Fact] public void PixelColorType_Other_ShouldBeSet() { @@ -165,15 +165,16 @@ public class PixelColorTypeTests public void PixelColorType_RGB_ShouldNotContainOtherFlags() { const PixelColorType colorType = PixelColorType.RGB; - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -181,15 +182,16 @@ public class PixelColorTypeTests public void PixelColorType_BGR_ShouldNotContainOtherFlags() { const PixelColorType colorType = PixelColorType.BGR; - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -201,12 +203,12 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Green)); Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); Assert.False(colorType.HasFlag(PixelColorType.Key)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -218,11 +220,11 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Green)); Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -234,11 +236,11 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Green)); Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); Assert.False(colorType.HasFlag(PixelColorType.Yellow)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Other)); } @@ -250,10 +252,10 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Green)); Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); Assert.False(colorType.HasFlag(PixelColorType.RGB)); Assert.False(colorType.HasFlag(PixelColorType.BGR)); - Assert.False(colorType.HasFlag(PixelColorType.Luminance)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); Assert.False(colorType.HasFlag(PixelColorType.YCbCr)); @@ -274,10 +276,11 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Green)); Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); - Assert.False(colorType.HasFlag(PixelColorType.Grayscale)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.RGB)); Assert.False(colorType.HasFlag(PixelColorType.BGR)); - Assert.False(colorType.HasFlag(PixelColorType.Luminance)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); Assert.False(colorType.HasFlag(PixelColorType.YCbCr)); @@ -287,6 +290,5 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Key)); Assert.False(colorType.HasFlag(PixelColorType.CMYK)); Assert.False(colorType.HasFlag(PixelColorType.YCCK)); - Assert.False(colorType.HasFlag(PixelColorType.Indexed)); } } From 8166213c17895ac8159d5b898deeba843441c0f8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 May 2024 16:55:43 +1000 Subject: [PATCH 07/43] Implement GifFrameMetadata --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 4 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 4 +- .../Formats/Gif/GifDisposalMethod.cs | 37 ---------- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 12 ++-- .../Formats/Gif/GifFrameMetadata.cs | 71 +++++++++++++++---- src/ImageSharp/Formats/Gif/GifMetadata.cs | 4 +- .../Formats/Gif/MetadataExtensions.cs | 12 +--- .../Sections/GifGraphicControlExtension.cs | 6 +- .../Formats/Gif/GifEncoderTests.cs | 8 +-- .../Formats/Gif/GifFrameMetadataTests.cs | 5 +- .../Formats/Gif/GifMetadataTests.cs | 4 +- .../GifGraphicControlExtensionTests.cs | 9 +-- .../Formats/Png/PngEncoderTests.cs | 8 +-- .../Formats/WebP/WebpEncoderTests.cs | 9 +-- .../Metadata/ImageFrameMetadataTests.cs | 3 +- 15 files changed, 101 insertions(+), 95 deletions(-) delete mode 100644 src/ImageSharp/Formats/Gif/GifDisposalMethod.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 2f03b9c6f3..00c5910d4b 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -138,8 +138,8 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata new(); /// - public IDeepCloneable DeepClone() => ((IDeepCloneable)this).DeepClone(); + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - BmpMetadata IDeepCloneable.DeepClone() => new(this); + public BmpMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index a67ee8f982..e0fe4973dd 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -517,7 +517,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } else { - if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) + if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious) { prevFrame = previousFrame; } @@ -624,7 +624,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals previousFrame = currentFrame ?? image.Frames.RootFrame; - if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground) + if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToBackground) { this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); } diff --git a/src/ImageSharp/Formats/Gif/GifDisposalMethod.cs b/src/ImageSharp/Formats/Gif/GifDisposalMethod.cs deleted file mode 100644 index 12b4239c4f..0000000000 --- a/src/ImageSharp/Formats/Gif/GifDisposalMethod.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Gif; - -/// -/// Provides enumeration for instructing the decoder what to do with the last image -/// in an animation sequence. -/// section 23 -/// -public enum GifDisposalMethod -{ - /// - /// No disposal specified. - /// The decoder is not required to take any action. - /// - Unspecified = 0, - - /// - /// Do not dispose. - /// The graphic is to be left in place. - /// - NotDispose = 1, - - /// - /// Restore to background color. - /// The area used by the graphic must be restored to the background color. - /// - RestoreToBackground = 2, - - /// - /// Restore to previous. - /// The decoder is required to restore the area overwritten by the - /// graphic with what was there prior to rendering the graphic. - /// - RestoreToPrevious = 3 -} diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 70afe12e10..f4cc166fec 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -235,7 +235,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals Image image, ReadOnlyMemory globalPalette, int globalTransparencyIndex, - GifDisposalMethod previousDisposalMethod) + FrameDisposalMode previousDisposalMode) where TPixel : unmanaged, IPixel { if (image.Frames.Count == 1) @@ -279,10 +279,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals useLocal, gifMetadata, paletteQuantizer, - previousDisposalMethod); + previousDisposalMode); previousFrame = currentFrame; - previousDisposalMethod = gifMetadata.DisposalMethod; + previousDisposalMode = gifMetadata.DisposalMethod; } if (hasPaletteQuantizer) @@ -323,14 +323,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals bool useLocal, GifFrameMetadata metadata, PaletteQuantizer globalPaletteQuantizer, - GifDisposalMethod previousDisposal) + FrameDisposalMode previousDisposalMode) where TPixel : unmanaged, IPixel { // Capture any explicit transparency index from the metadata. // We use it to determine the value to use to replace duplicate pixels. int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1; - ImageFrame? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame; + ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame; // Deduplicate and quantize the frame capturing only required parts. (bool difference, Rectangle bounds) = @@ -664,7 +664,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals bool hasTransparency = metadata.HasTransparency; byte packedValue = GifGraphicControlExtension.GetPackedValue( - disposalMethod: metadata.DisposalMethod, + disposalMode: metadata.DisposalMethod, transparencyFlag: hasTransparency); GifGraphicControlExtension extension = new( diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index 3f8563706d..46bc415ce1 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Provides Gif specific metadata information for the image frame. /// -public class GifFrameMetadata : IDeepCloneable +public class GifFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -73,10 +73,65 @@ public class GifFrameMetadata : IDeepCloneable /// Primarily used in Gif animation, this field indicates the way in which the graphic is to /// be treated after being displayed. /// - public GifDisposalMethod DisposalMethod { get; set; } + public FrameDisposalMode DisposalMethod { get; set; } + + /// + public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + int index = -1; + const float background = 1f; + if (metadata.ColorTable.HasValue) + { + ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; + for (int i = 0; i < colorTable.Length; i++) + { + Vector4 vector = colorTable[i].ToScaledVector4(); + if (vector.W < background) + { + index = i; + } + } + } + + bool hasTransparency = index >= 0; + + return new() + { + LocalColorTable = metadata.ColorTable, + ColorTableMode = metadata.ColorTableMode, + FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), + DisposalMethod = metadata.DisposalMode, + HasTransparency = hasTransparency, + TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + // throw new NotImplementedException(); + // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or + // has a local palette with 256 colors and is not transparent we should use 'Source'. + bool blendSource = this.DisposalMethod == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency); + + // If the color table is global and frame has no transparency. Consider it 'Source' also. + blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency; + + return new() + { + ColorTable = this.LocalColorTable, + ColorTableMode = this.ColorTableMode, + Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10), + DisposalMode = this.DisposalMethod, + BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - public IDeepCloneable DeepClone() => new GifFrameMetadata(this); + public GifFrameMetadata DeepClone() => new(this); internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) { @@ -103,17 +158,9 @@ public class GifFrameMetadata : IDeepCloneable LocalColorTable = metadata.ColorTable, ColorTableMode = metadata.ColorTableMode, FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMethod = GetMode(metadata.DisposalMode), + DisposalMethod = metadata.DisposalMode, HasTransparency = hasTransparency, TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, }; } - - private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch - { - FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose, - FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground, - FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious, - _ => GifDisposalMethod.Unspecified, - }; } diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 43935504b1..4ebe046ba3 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -145,8 +145,8 @@ public class GifMetadata : IFormatMetadata } /// - public IDeepCloneable DeepClone() => ((IDeepCloneable)this).DeepClone(); + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - GifMetadata IDeepCloneable.DeepClone() => new(this); + public GifMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index d4650403cb..5fd2d5c1e9 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -80,7 +80,7 @@ public static partial class MetadataExtensions { // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or // has a local palette with 256 colors and is not transparent we should use 'Source'. - bool blendSource = source.DisposalMethod == GifDisposalMethod.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); + bool blendSource = source.DisposalMethod == FrameDisposalMode.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); // If the color table is global and frame has no transparency. Consider it 'Source' also. blendSource |= source.ColorTableMode == FrameColorTableMode.Global && !source.HasTransparency; @@ -90,16 +90,8 @@ public static partial class MetadataExtensions ColorTable = source.LocalColorTable, ColorTableMode = source.ColorTableMode, Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), - DisposalMode = GetMode(source.DisposalMethod), + DisposalMode = source.DisposalMethod, BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, }; } - - private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch - { - GifDisposalMethod.NotDispose => FrameDisposalMode.DoNotDispose, - GifDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - GifDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, - _ => FrameDisposalMode.Unspecified, - }; } diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs index 52052021fc..ad99ac0f42 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -52,7 +52,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable< /// Gets the disposal method which indicates the way in which the /// graphic is to be treated after being displayed. /// - public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2); + public FrameDisposalMode DisposalMethod => (FrameDisposalMode)((this.Packed & 0x1C) >> 2); /// /// Gets a value indicating whether transparency flag is to be set. @@ -80,7 +80,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable< public static GifGraphicControlExtension Parse(ReadOnlySpan buffer) => MemoryMarshal.Cast(buffer)[0]; - public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) + public static byte GetPackedValue(FrameDisposalMode disposalMode, bool userInputFlag = false, bool transparencyFlag = false) { /* Reserved | 3 Bits @@ -91,7 +91,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable< byte value = 0; - value |= (byte)((int)disposalMethod << 2); + value |= (byte)((int)disposalMode << 2); if (userInputFlag) { diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 767141f56a..0b6615b8ea 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -309,11 +309,11 @@ public class GifEncoderTests switch (pngF.DisposalMethod) { case PngDisposalMethod.RestoreToBackground: - Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod); break; case PngDisposalMethod.DoNotDispose: default: - Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod); break; } } @@ -359,11 +359,11 @@ public class GifEncoderTests switch (webpF.DisposalMethod) { case WebpDisposalMethod.RestoreToBackground: - Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod); break; case WebpDisposalMethod.DoNotDispose: default: - Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod); break; } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs index 774638311d..f92896b7ec 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Tests.Formats.Gif; @@ -14,14 +15,14 @@ public class GifFrameMetadataTests GifFrameMetadata meta = new() { FrameDelay = 1, - DisposalMethod = GifDisposalMethod.RestoreToBackground, + DisposalMethod = FrameDisposalMode.RestoreToBackground, LocalColorTable = new[] { Color.Black, Color.White } }; GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone(); clone.FrameDelay = 2; - clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; + clone.DisposalMethod = FrameDisposalMode.RestoreToPrevious; clone.LocalColorTable = new[] { Color.Black }; Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index c7babe9322..afaa827bfb 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -183,14 +183,14 @@ public class GifMetadataTests } [Theory] - [InlineData(TestImages.Gif.Cheers, 93, FrameColorTableMode.Global, 256, 4, GifDisposalMethod.NotDispose)] + [InlineData(TestImages.Gif.Cheers, 93, FrameColorTableMode.Global, 256, 4, FrameDisposalMode.DoNotDispose)] public void Identify_Frames( string imagePath, int framesCount, FrameColorTableMode colorTableMode, int globalColorTableLength, int frameDelay, - GifDisposalMethod disposalMethod) + FrameDisposalMode disposalMethod) { TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); diff --git a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs index c602bc91bb..05dc5bc52a 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Tests.Formats.Gif.Sections; @@ -10,9 +11,9 @@ public class GifGraphicControlExtensionTests [Fact] public void TestPackedValue() { - Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.Unspecified, false, false)); - Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToBackground, true, true)); - Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.NotDispose, false, false)); - Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToPrevious, true, false)); + Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.Unspecified, false, false)); + Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.RestoreToBackground, true, true)); + Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.DoNotDispose, false, false)); + Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.RestoreToPrevious, true, false)); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 35c446c704..1e4472e8a2 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -521,14 +521,14 @@ public partial class PngEncoderTests switch (gifF.DisposalMethod) { - case GifDisposalMethod.RestoreToBackground: + case FrameDisposalMode.RestoreToBackground: Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod); break; - case GifDisposalMethod.RestoreToPrevious: + case FrameDisposalMode.RestoreToPrevious: Assert.Equal(PngDisposalMethod.RestoreToPrevious, pngF.DisposalMethod); break; - case GifDisposalMethod.Unspecified: - case GifDisposalMethod.NotDispose: + case FrameDisposalMode.Unspecified: + case FrameDisposalMode.DoNotDispose: default: Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod); break; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index acca49dcf4..49c47adfd4 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; @@ -96,12 +97,12 @@ public class WebpEncoderTests switch (gifF.DisposalMethod) { - case GifDisposalMethod.RestoreToBackground: + case FrameDisposalMode.RestoreToBackground: Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); break; - case GifDisposalMethod.RestoreToPrevious: - case GifDisposalMethod.Unspecified: - case GifDisposalMethod.NotDispose: + case FrameDisposalMode.RestoreToPrevious: + case FrameDisposalMode.Unspecified: + case FrameDisposalMode.DoNotDispose: default: Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); break; diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index bcc9675404..9cb7137fe0 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -21,7 +22,7 @@ public class ImageFrameMetadataTests { const int frameDelay = 42; const int colorTableLength = 128; - const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; + const FrameDisposalMode disposalMethod = FrameDisposalMode.RestoreToBackground; ImageFrameMetadata metaData = new(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); From 5b47e790626446593bca190a462178f91b51cbf5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 May 2024 23:59:34 +1000 Subject: [PATCH 08/43] Implement JpegMetadata --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 96 ++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index ce4c0758fd..b12ee524d9 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -2,18 +2,19 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg; /// /// Provides Jpeg specific metadata information for the image. /// -public class JpegMetadata : IDeepCloneable +public class JpegMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. /// - public JpegMetadata() => this.Comments = new List(); + public JpegMetadata() => this.Comments = []; /// /// Initializes a new instance of the class. @@ -99,5 +100,94 @@ public class JpegMetadata : IDeepCloneable public IList Comments { get; } /// - public IDeepCloneable DeepClone() => new JpegMetadata(this); + public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + JpegEncodingColor color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.YCbCr; + switch (colorType) + { + case PixelColorType.Luminance: + color = JpegEncodingColor.Luminance; + break; + case PixelColorType.CMYK: + color = JpegEncodingColor.Cmyk; + break; + case PixelColorType.YCCK: + color = JpegEncodingColor.Ycck; + break; + default: + if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR)) + { + color = JpegEncodingColor.Rgb; + } + else + { + color = metadata.Quality <= Quantization.DefaultQualityFactor + ? JpegEncodingColor.YCbCrRatio420 + : JpegEncodingColor.YCbCrRatio444; + } + + break; + } + + return new JpegMetadata + { + ColorType = color, + ChrominanceQuality = metadata.Quality, + LuminanceQuality = metadata.Quality, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + int bpp; + PixelColorType colorType; + PixelComponentInfo info; + switch (this.ColorType) + { + case JpegEncodingColor.Luminance: + bpp = 8; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, 8); + break; + case JpegEncodingColor.Cmyk: + bpp = 32; + colorType = PixelColorType.CMYK; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + case JpegEncodingColor.Ycck: + bpp = 32; + colorType = PixelColorType.YCCK; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + case JpegEncodingColor.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + default: + bpp = 24; + colorType = PixelColorType.YCbCr; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + } + + return new FormatConnectingMetadata + { + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, + }, + Quality = this.Quality, + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public JpegMetadata DeepClone() => new(this); } From 2127b46a04a8147c9eb02ff362f3617466265891 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 May 2024 21:02:39 +1000 Subject: [PATCH 09/43] Implement PngFrameMetadata --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 10 +- .../Formats/Gif/GifFrameMetadata.cs | 13 +- src/ImageSharp/Formats/Gif/GifMetadata.cs | 8 +- .../Formats/Gif/MetadataExtensions.cs | 4 +- src/ImageSharp/Formats/IImageFormat.cs | 6 +- src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 97 ++++++++++- .../Formats/Png/Chunks/FrameControl.cs | 20 +-- .../Formats/Png/MetadataExtensions.cs | 14 +- src/ImageSharp/Formats/Png/PngBlendMethod.cs | 22 --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 41 +++-- .../Formats/Png/PngDisposalMethod.cs | 25 --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 17 +- .../Formats/Png/PngFrameMetadata.cs | 64 +++++-- src/ImageSharp/Formats/Png/PngMetadata.cs | 163 +++++++++++++++++- src/ImageSharp/Metadata/ImageMetadata.cs | 2 +- src/ImageSharp/PixelFormats/PixelColorType.cs | 31 ++-- .../Formats/Gif/GifEncoderTests.cs | 14 +- .../Formats/Gif/GifFrameMetadataTests.cs | 6 +- .../Formats/Gif/GifMetadataTests.cs | 2 +- .../Formats/Png/PngEncoderTests.cs | 20 +-- .../Formats/Png/PngFrameMetadataTests.cs | 19 +- .../Formats/WebP/WebpEncoderTests.cs | 14 +- .../ImageFrameCollectionTests.NonGeneric.cs | 2 +- .../Metadata/ImageFrameMetadataTests.cs | 4 +- .../PixelFormats/PixelColorTypeTests.cs | 39 +++++ 26 files changed, 475 insertions(+), 184 deletions(-) delete mode 100644 src/ImageSharp/Formats/Png/PngBlendMethod.cs delete mode 100644 src/ImageSharp/Formats/Png/PngDisposalMethod.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e0fe4973dd..e110acc30d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -719,7 +719,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag; gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; - gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; + gifMeta.DisposalMode = this.graphicsControlExtension.DisposalMethod; } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index f4cc166fec..5fd20d6ae2 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -171,7 +171,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); - this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMethod); + this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode); stream.WriteByte(GifConstants.EndIntroducer); @@ -183,7 +183,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals { if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) { - return (GifMetadata)gif.DeepClone(); + return gif.DeepClone(); } if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) @@ -208,7 +208,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals GifFrameMetadata? metadata = null; if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) { - metadata = (GifFrameMetadata)gif.DeepClone(); + metadata = gif.DeepClone(); } else if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) { @@ -282,7 +282,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals previousDisposalMode); previousFrame = currentFrame; - previousDisposalMode = gifMetadata.DisposalMethod; + previousDisposalMode = gifMetadata.DisposalMode; } if (hasPaletteQuantizer) @@ -664,7 +664,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals bool hasTransparency = metadata.HasTransparency; byte packedValue = GifGraphicControlExtension.GetPackedValue( - disposalMode: metadata.DisposalMethod, + disposalMode: metadata.DisposalMode, transparencyFlag: hasTransparency); GifGraphicControlExtension extension = new( diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index 46bc415ce1..f81329e973 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -26,7 +26,7 @@ public class GifFrameMetadata : IFormatFrameMetadata { this.ColorTableMode = other.ColorTableMode; this.FrameDelay = other.FrameDelay; - this.DisposalMethod = other.DisposalMethod; + this.DisposalMode = other.DisposalMode; if (other.LocalColorTable?.Length > 0) { @@ -73,7 +73,7 @@ public class GifFrameMetadata : IFormatFrameMetadata /// Primarily used in Gif animation, this field indicates the way in which the graphic is to /// be treated after being displayed. /// - public FrameDisposalMode DisposalMethod { get; set; } + public FrameDisposalMode DisposalMode { get; set; } /// public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) @@ -100,7 +100,7 @@ public class GifFrameMetadata : IFormatFrameMetadata LocalColorTable = metadata.ColorTable, ColorTableMode = metadata.ColorTableMode, FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMethod = metadata.DisposalMode, + DisposalMode = metadata.DisposalMode, HasTransparency = hasTransparency, TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, }; @@ -109,10 +109,9 @@ public class GifFrameMetadata : IFormatFrameMetadata /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() { - // throw new NotImplementedException(); // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or // has a local palette with 256 colors and is not transparent we should use 'Source'. - bool blendSource = this.DisposalMethod == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency); + bool blendSource = this.DisposalMode == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency); // If the color table is global and frame has no transparency. Consider it 'Source' also. blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency; @@ -122,7 +121,7 @@ public class GifFrameMetadata : IFormatFrameMetadata ColorTable = this.LocalColorTable, ColorTableMode = this.ColorTableMode, Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10), - DisposalMode = this.DisposalMethod, + DisposalMode = this.DisposalMode, BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, }; } @@ -158,7 +157,7 @@ public class GifFrameMetadata : IFormatFrameMetadata LocalColorTable = metadata.ColorTable, ColorTableMode = metadata.ColorTableMode, FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMethod = metadata.DisposalMode, + DisposalMode = metadata.DisposalMode, HasTransparency = hasTransparency, TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, }; diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 4ebe046ba3..9a234aa9ba 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -129,16 +129,20 @@ public class GifMetadata : IFormatMetadata ? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex] : Color.Transparent; + int bpp = this.GlobalColorTable.HasValue + ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8) + : 8; + return new() { AnimateRootFrame = true, BackgroundColor = color, ColorTable = this.GlobalColorTable, ColorTableMode = this.ColorTableMode, - PixelTypeInfo = new PixelTypeInfo(24) + PixelTypeInfo = new PixelTypeInfo(bpp) { ColorType = PixelColorType.Indexed, - ComponentInfo = PixelComponentInfo.Create(3, 24, 8, 8, 8), + ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp), }, RepeatCount = this.RepeatCount, }; diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index 5fd2d5c1e9..0f2d281f16 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -80,7 +80,7 @@ public static partial class MetadataExtensions { // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or // has a local palette with 256 colors and is not transparent we should use 'Source'. - bool blendSource = source.DisposalMethod == FrameDisposalMode.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); + bool blendSource = source.DisposalMode == FrameDisposalMode.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); // If the color table is global and frame has no transparency. Consider it 'Source' also. blendSource |= source.ColorTableMode == FrameColorTableMode.Global && !source.HasTransparency; @@ -90,7 +90,7 @@ public static partial class MetadataExtensions ColorTable = source.LocalColorTable, ColorTableMode = source.ColorTableMode, Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), - DisposalMode = source.DisposalMethod, + DisposalMode = source.DisposalMode, BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, }; } diff --git a/src/ImageSharp/Formats/IImageFormat.cs b/src/ImageSharp/Formats/IImageFormat.cs index b983219777..5c579eb4f6 100644 --- a/src/ImageSharp/Formats/IImageFormat.cs +++ b/src/ImageSharp/Formats/IImageFormat.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats; @@ -14,12 +14,12 @@ public interface IImageFormat string Name { get; } /// - /// Gets the default mimetype that the image format uses + /// Gets the default mime type that the image format uses /// string DefaultMimeType { get; } /// - /// Gets all the mimetypes that have been used by this image format. + /// Gets all the mime types that have been used by this image format. /// IEnumerable MimeTypes { get; } diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index 938e11db16..4e6b6f2650 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Provides PBM specific metadata information for the image. /// -public class PbmMetadata : IDeepCloneable +public class PbmMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -38,8 +40,97 @@ public class PbmMetadata : IDeepCloneable /// /// Gets or sets the data type of the pixel components. /// - public PbmComponentType ComponentType { get; set; } + public PbmComponentType ComponentType { get; set; } = PbmComponentType.Byte; + + /// + public static PbmMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PbmColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.Luminance; + + switch (colorType) + { + case PixelColorType.Binary: + color = PbmColorType.BlackAndWhite; + break; + case PixelColorType.Luminance: + color = PbmColorType.Grayscale; + break; + default: + if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR)) + { + color = PbmColorType.Rgb; + } + else + { + color = PbmColorType.Grayscale; + } + + break; + } + + PbmComponentType componentType = PbmComponentType.Short; + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + if (bpp == 1) + { + componentType = PbmComponentType.Bit; + } + else if (bpp <= 8) + { + componentType = PbmComponentType.Byte; + } + + return new PbmMetadata + { + ColorType = color, + ComponentType = componentType + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + int bpp; + PixelColorType colorType; + PixelComponentInfo info; + switch (this.ColorType) + { + case PbmColorType.BlackAndWhite: + bpp = 1; + colorType = PixelColorType.Binary; + info = PixelComponentInfo.Create(1, bpp, 1); + break; + case PbmColorType.Grayscale: + bpp = 8; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, 8); + break; + case PbmColorType.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + default: + bpp = 8; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, 8); + break; + } + + return new FormatConnectingMetadata + { + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, + }, + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - public IDeepCloneable DeepClone() => new PbmMetadata(this); + public PbmMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index fb2ca473c2..91f79c8154 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -22,8 +22,8 @@ internal readonly struct FrameControl uint yOffset, ushort delayNumerator, ushort delayDenominator, - PngDisposalMethod disposeOperation, - PngBlendMethod blendOperation) + FrameDisposalMode disposalMode, + FrameBlendMode blendMode) { this.SequenceNumber = sequenceNumber; this.Width = width; @@ -32,8 +32,8 @@ internal readonly struct FrameControl this.YOffset = yOffset; this.DelayNumerator = delayNumerator; this.DelayDenominator = delayDenominator; - this.DisposeOperation = disposeOperation; - this.BlendOperation = blendOperation; + this.DisposalMode = disposalMode; + this.BlendMode = blendMode; } /// @@ -84,12 +84,12 @@ internal readonly struct FrameControl /// /// Gets the type of frame area disposal to be done after rendering this frame /// - public PngDisposalMethod DisposeOperation { get; } + public FrameDisposalMode DisposalMode { get; } /// /// Gets the type of frame area rendering for this frame /// - public PngBlendMethod BlendOperation { get; } + public FrameBlendMode BlendMode { get; } public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height); @@ -137,8 +137,8 @@ internal readonly struct FrameControl BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator); BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator); - buffer[24] = (byte)this.DisposeOperation; - buffer[25] = (byte)this.BlendOperation; + buffer[24] = (byte)(this.DisposalMode - 1); + buffer[25] = (byte)this.BlendMode; } /// @@ -155,6 +155,6 @@ internal readonly struct FrameControl yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]), delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]), delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]), - disposeOperation: (PngDisposalMethod)data[24], - blendOperation: (PngBlendMethod)data[25]); + disposalMode: (FrameDisposalMode)(data[24] + 1), + blendMode: (FrameBlendMode)data[25]); } diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs index b6313bffe0..6da1fb5fdb 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -69,16 +69,16 @@ public static partial class MetadataExtensions { ColorTableMode = FrameColorTableMode.Global, Duration = TimeSpan.FromMilliseconds(delay * 1000), - DisposalMode = GetMode(source.DisposalMethod), - BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over, + DisposalMode = GetMode(source.DisposalMode), + BlendMode = source.BlendMode, }; } - private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch + private static FrameDisposalMode GetMode(FrameDisposalMode method) => method switch { - PngDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose, - PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, - _ => FrameDisposalMode.Unspecified, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, + _ => FrameDisposalMode.DoNotDispose, }; } diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs deleted file mode 100644 index f71dce8325..0000000000 --- a/src/ImageSharp/Formats/Png/PngBlendMethod.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Png; - -/// -/// Specifies whether the frame is to be alpha blended into the current output buffer content, -/// or whether it should completely replace its region in the output buffer. -/// -public enum PngBlendMethod -{ - /// - /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. - /// - Source, - - /// - /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as - /// described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. - /// - Over -} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 36a0a8bcbb..a4d13177d8 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -246,7 +246,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals cancellationToken); // if current frame dispose is restore to previous, then from future frame's perspective, it never happened - if (currentFrameControl.Value.DisposeOperation != PngDisposalMethod.RestoreToPrevious) + if (currentFrameControl.Value.DisposalMode != FrameDisposalMode.RestoreToPrevious) { previousFrame = currentFrame; previousFrameControl = currentFrameControl; @@ -659,12 +659,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. // So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null). - if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious)) + if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToPrevious)) { Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(); pixelRegion.Clear(); } - else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground) + else if (previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToBackground) { Rectangle restoreArea = previousFrameControl.Value.Bounds; Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(restoreArea); @@ -794,8 +794,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int height = (int)frameControl.YMax; IMemoryOwner? blendMemory = null; - Span blendRowBuffer = Span.Empty; - if (frameControl.BlendOperation == PngBlendMethod.Over) + Span blendRowBuffer = []; + if (frameControl.BlendMode == FrameBlendMode.Over) { blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); blendRowBuffer = blendMemory.Memory.Span; @@ -887,8 +887,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals Buffer2D imageBuffer = imageFrame.PixelBuffer; IMemoryOwner? blendMemory = null; - Span blendRowBuffer = Span.Empty; - if (frameControl.BlendOperation == PngBlendMethod.Over) + Span blendRowBuffer = []; + if (frameControl.BlendMode == FrameBlendMode.Over) { blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); blendRowBuffer = blendMemory.Memory.Span; @@ -1013,7 +1013,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); - bool blend = frameControl.BlendOperation == PngBlendMethod.Over; + bool blend = frameControl.BlendMode == FrameBlendMode.Over; Span rowSpan = blend ? blendRowBuffer : destination; @@ -1126,7 +1126,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int increment = 1) where TPixel : unmanaged, IPixel { - bool blend = frameControl.BlendOperation == PngBlendMethod.Over; + bool blend = frameControl.BlendMode == FrameBlendMode.Over; Span rowSpan = blend ? blendRowBuffer : destination; @@ -1460,7 +1460,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals byte colorPrimaries = data[0]; byte transferFunction = data[1]; byte matrixCoefficients = data[2]; - bool? fullRange = data[3] == 1 ? true : data[3] == 0 ? false : null; + bool? fullRange; + if (data[3] == 1) + { + fullRange = true; + } + else if (data[3] == 0) + { + fullRange = false; + } + else + { + fullRange = null; + } + metadata.CicpProfile = new CicpProfile(colorPrimaries, transferFunction, matrixCoefficients, fullRange); } @@ -1492,7 +1505,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes). // This doesn't actually allocate. - ReadOnlySpan exifHeader = new byte[] { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 }; + ReadOnlySpan exifHeader = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; if (dataLength < exifHeader.Length) { @@ -1603,7 +1616,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals Span destUncompressedData = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { - uncompressedBytesArray = Array.Empty(); + uncompressedBytesArray = []; return false; } @@ -1612,7 +1625,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { if (memoryStreamOutput.Length > maxLength) { - uncompressedBytesArray = Array.Empty(); + uncompressedBytesArray = []; return false; } @@ -1979,7 +1992,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { if (length == 0) { - return new BasicArrayBuffer(Array.Empty()); + return new BasicArrayBuffer([]); } // We rent the buffer here to return it afterwards in Decode() diff --git a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs deleted file mode 100644 index 1537c5cedf..0000000000 --- a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Png; - -/// -/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). -/// -public enum PngDisposalMethod -{ - /// - /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. - /// - DoNotDispose, - - /// - /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. - /// - RestoreToBackground, - - /// - /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. - /// - RestoreToPrevious -} diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 36ce136f47..49157272c9 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -212,7 +212,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // Write the first animated frame. currentFrame = image.Frames[currentFrameIndex]; PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); - PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); uint sequenceNumber = 1; if (pngMetadata.AnimateRootFrame) @@ -237,12 +237,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { - ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; currentFrame = image.Frames[currentFrameIndex]; ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; frameMetadata = GetPngFrameMetadata(currentFrame); - bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -268,7 +268,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; + previousDisposal = frameMetadata.DisposalMode; } } @@ -293,7 +293,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) { - return (PngMetadata)png.DeepClone(); + return png.DeepClone(); } if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) @@ -317,7 +317,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) { - return (PngFrameMetadata)png.DeepClone(); + return png.DeepClone(); } if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) @@ -881,6 +881,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The containing image data. /// The image meta data. + /// CICP matrix coefficients other than Identity are not supported in PNG. private void WriteCicpChunk(Stream stream, ImageMetadata metaData) { if (metaData.CicpProfile is null) @@ -1125,8 +1126,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable yOffset: (uint)bounds.Top, delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, - disposeOperation: frameMetadata.DisposalMethod, - blendOperation: frameMetadata.BlendMethod); + disposalMode: frameMetadata.DisposalMode, + blendMode: frameMetadata.BlendMode); fcTL.WriteTo(this.chunkDataBuffer.Span); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index dbda4d73c9..de141909ad 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Provides APng specific metadata information for the image frame. /// -public class PngFrameMetadata : IDeepCloneable +public class PngFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -24,8 +24,8 @@ public class PngFrameMetadata : IDeepCloneable private PngFrameMetadata(PngFrameMetadata other) { this.FrameDelay = other.FrameDelay; - this.DisposalMethod = other.DisposalMethod; - this.BlendMethod = other.BlendMethod; + this.DisposalMode = other.DisposalMode; + this.BlendMode = other.BlendMode; } /// @@ -39,12 +39,12 @@ public class PngFrameMetadata : IDeepCloneable /// /// Gets or sets the type of frame area disposal to be done after rendering this frame /// - public PngDisposalMethod DisposalMethod { get; set; } + public FrameDisposalMode DisposalMode { get; set; } /// /// Gets or sets the type of frame area rendering for this frame /// - public PngBlendMethod BlendMethod { get; set; } + public FrameBlendMode BlendMode { get; set; } /// /// Initializes a new instance of the class. @@ -53,26 +53,56 @@ public class PngFrameMetadata : IDeepCloneable internal void FromChunk(in FrameControl frameControl) { this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator); - this.DisposalMethod = frameControl.DisposeOperation; - this.BlendMethod = frameControl.BlendOperation; + this.DisposalMode = frameControl.DisposalMode; + this.BlendMode = frameControl.BlendMode; } - /// - public IDeepCloneable DeepClone() => new PngFrameMetadata(this); - internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) => new() { FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), - DisposalMethod = GetMode(metadata.DisposalMode), - BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over, + DisposalMode = GetMode(metadata.DisposalMode), + BlendMode = metadata.BlendMode, }; - private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch + private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch { - FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground, - FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious, - FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose, - _ => PngDisposalMethod.DoNotDispose, + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + _ => FrameDisposalMode.DoNotDispose, }; + + /// + public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new() + { + FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), + DisposalMode = GetMode(metadata.DisposalMode), + BlendMode = metadata.BlendMode, + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + double delay = this.FrameDelay.ToDouble(); + if (double.IsNaN(delay)) + { + delay = 0; + } + + return new() + { + ColorTableMode = FrameColorTableMode.Global, + Duration = TimeSpan.FromMilliseconds(delay * 1000), + DisposalMode = this.DisposalMode, + BlendMode = this.BlendMode, + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public PngFrameMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index de02e390f0..49799f54b2 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -2,13 +2,14 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; /// /// Provides Png specific metadata information for the image. /// -public class PngMetadata : IDeepCloneable +public class PngMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -89,9 +90,6 @@ public class PngMetadata : IDeepCloneable /// public bool AnimateRootFrame { get; set; } = true; - /// - public IDeepCloneable DeepClone() => new PngMetadata(this); - internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) { // Should the conversion be from a format that uses a 24bit palette entries (gif) @@ -121,4 +119,161 @@ public class PngMetadata : IDeepCloneable RepeatCount = metadata.RepeatCount, }; } + + /// + public static PngMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + // Should the conversion be from a format that uses a 24bit palette entries (gif) + // we need to clone and adjust the color table to allow for transparency. + Color[]? colorTable = metadata.ColorTable?.ToArray(); + if (colorTable != null) + { + for (int i = 0; i < colorTable.Length; i++) + { + ref Color c = ref colorTable[i]; + if (c == metadata.BackgroundColor) + { + // Png treats background as fully empty + c = Color.Transparent; + break; + } + } + } + + PngColorType color; + PixelColorType colorType = + metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB | PixelColorType.Alpha; + + switch (colorType) + { + case PixelColorType.Binary: + case PixelColorType.Indexed: + color = PngColorType.Palette; + break; + case PixelColorType.Luminance: + color = PngColorType.Grayscale; + break; + case PixelColorType.RGB: + case PixelColorType.BGR: + color = PngColorType.Rgb; + break; + default: + if (colorType.HasFlag(PixelColorType.Luminance)) + { + color = PngColorType.GrayscaleWithAlpha; + break; + } + + color = PngColorType.RgbWithAlpha; + break; + } + + // PNG uses bits per component not per pixel. + int bpc = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8; + PngBitDepth bitDepth = bpc switch + { + 1 => PngBitDepth.Bit1, + 2 => PngBitDepth.Bit2, + 4 => PngBitDepth.Bit4, + _ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16, + }; + return new() + { + ColorType = color, + BitDepth = bitDepth, + ColorTable = colorTable, + RepeatCount = metadata.RepeatCount, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + switch (this.ColorType) + { + case PngColorType.Palette: + bpp = this.ColorTable.HasValue + ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.ColorTable.Value.Length), 1, 8) + : 8; + + colorType = PixelColorType.Indexed; + info = PixelComponentInfo.Create(1, bpp, bpp); + break; + + case PngColorType.Grayscale: + bpp = 8; + colorType = PixelColorType.Luminance; + info = PixelComponentInfo.Create(1, bpp, 8); + break; + + case PngColorType.GrayscaleWithAlpha: + + alpha = PixelAlphaRepresentation.Unassociated; + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 32; + colorType = PixelColorType.Luminance | PixelColorType.Alpha; + info = PixelComponentInfo.Create(2, bpp, 16, 16); + break; + } + + bpp = 16; + colorType = PixelColorType.Luminance | PixelColorType.Alpha; + info = PixelComponentInfo.Create(2, bpp, 8, 8); + break; + + case PngColorType.Rgb: + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + } + + bpp = 48; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 16, 16, 16); + break; + + default: + + alpha = PixelAlphaRepresentation.Unassociated; + if (this.BitDepth == PngBitDepth.Bit16) + { + bpp = 64; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 16, 16, 16, 16); + break; + } + + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + break; + } + + return new() + { + ColorTable = this.ColorTable, + ColorTableMode = FrameColorTableMode.Global, + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }, + RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public PngMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 6b62be08ff..ceeea42cfc 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -33,7 +33,7 @@ public sealed class ImageMetadata : IDeepCloneable /// public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch; - private readonly Dictionary formatMetadata = new(); + private readonly Dictionary formatMetadata = []; private double horizontalResolution; private double verticalResolution; diff --git a/src/ImageSharp/PixelFormats/PixelColorType.cs b/src/ImageSharp/PixelFormats/PixelColorType.cs index afbe6a4d3e..315c0f6584 100644 --- a/src/ImageSharp/PixelFormats/PixelColorType.cs +++ b/src/ImageSharp/PixelFormats/PixelColorType.cs @@ -44,55 +44,60 @@ public enum PixelColorType /// Luminance = 1 << 5, + /// + /// Indicates that the color is in binary (black and white) format. + /// + Binary = 1 << 6, + /// /// Indicates that the color is indexed using a palette. /// - Indexed = 1 << 6, + Indexed = 1 << 7, /// /// Indicates that the color is in RGB (Red, Green, Blue) format. /// - RGB = Red | Green | Blue | (1 << 7), + RGB = Red | Green | Blue | (1 << 8), /// /// Indicates that the color is in BGR (Blue, Green, Red) format. /// - BGR = Blue | Green | Red | (1 << 8), + BGR = Blue | Green | Red | (1 << 9), /// - /// Represents the Chrominance Blue component in YCbCr. + /// Represents the Chrominance Blue component. /// - ChrominanceBlue = 1 << 9, + ChrominanceBlue = 1 << 10, /// - /// Represents the Chrominance Red component in YCbCr. + /// Represents the Chrominance Red component. /// - ChrominanceRed = 1 << 10, + ChrominanceRed = 1 << 11, /// /// Indicates that the color is in YCbCr (Luminance, Chrominance Blue, Chrominance Red) format. /// - YCbCr = Luminance | ChrominanceBlue | ChrominanceRed, + YCbCr = Luminance | ChrominanceBlue | ChrominanceRed | (1 << 12), /// /// Represents the Cyan component in CMYK. /// - Cyan = 1 << 11, + Cyan = 1 << 13, /// /// Represents the Magenta component in CMYK. /// - Magenta = 1 << 12, + Magenta = 1 << 14, /// /// Represents the Yellow component in CMYK. /// - Yellow = 1 << 13, + Yellow = 1 << 15, /// /// Represents the Key (black) component in CMYK and YCCK. /// - Key = 1 << 14, + Key = 1 << 16, /// /// Indicates that the color is in CMYK (Cyan, Magenta, Yellow, Key) format. @@ -107,5 +112,5 @@ public enum PixelColorType /// /// Indicates that the color is of a type not specified in this enum. /// - Other = 1 << 16 + Other = 1 << 17 } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 0b6615b8ea..aeb3bab7bb 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -306,14 +306,14 @@ public class GifEncoderTests Assert.Equal((int)(pngF.FrameDelay.ToDouble() * 100), gifF.FrameDelay); - switch (pngF.DisposalMethod) + switch (pngF.DisposalMode) { - case PngDisposalMethod.RestoreToBackground: - Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod); + case FrameDisposalMode.RestoreToBackground: + Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMode); break; - case PngDisposalMethod.DoNotDispose: + case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMode); break; } } @@ -359,11 +359,11 @@ public class GifEncoderTests switch (webpF.DisposalMethod) { case WebpDisposalMethod.RestoreToBackground: - Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMode); break; case WebpDisposalMethod.DoNotDispose: default: - Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMode); break; } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs index f92896b7ec..f12e6e7e50 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs @@ -15,18 +15,18 @@ public class GifFrameMetadataTests GifFrameMetadata meta = new() { FrameDelay = 1, - DisposalMethod = FrameDisposalMode.RestoreToBackground, + DisposalMode = FrameDisposalMode.RestoreToBackground, LocalColorTable = new[] { Color.Black, Color.White } }; GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone(); clone.FrameDelay = 2; - clone.DisposalMethod = FrameDisposalMode.RestoreToPrevious; + clone.DisposalMode = FrameDisposalMode.RestoreToPrevious; clone.LocalColorTable = new[] { Color.Black }; Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); - Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); + Assert.False(meta.DisposalMode.Equals(clone.DisposalMode)); Assert.False(meta.LocalColorTable.Value.Length == clone.LocalColorTable.Value.Length); Assert.Equal(1, clone.LocalColorTable.Value.Length); } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index afaa827bfb..e61be8235d 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -212,6 +212,6 @@ public class GifMetadataTests } Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay); - Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod); + Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMode); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 1e4472e8a2..e61475b659 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -479,8 +479,8 @@ public partial class PngEncoderTests PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngMetadata(); Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay); - Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod); - Assert.Equal(originalFrameMetadata.DisposalMethod, outputFrameMetadata.DisposalMethod); + Assert.Equal(originalFrameMetadata.BlendMode, outputFrameMetadata.BlendMode); + Assert.Equal(originalFrameMetadata.DisposalMode, outputFrameMetadata.DisposalMode); } } @@ -519,18 +519,18 @@ public partial class PngEncoderTests Assert.Equal(gifF.FrameDelay, (int)(pngF.FrameDelay.ToDouble() * 100)); - switch (gifF.DisposalMethod) + switch (gifF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: - Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, pngF.DisposalMode); break; case FrameDisposalMode.RestoreToPrevious: - Assert.Equal(PngDisposalMethod.RestoreToPrevious, pngF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToPrevious, pngF.DisposalMode); break; case FrameDisposalMode.Unspecified: case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, pngF.DisposalMode); break; } } @@ -570,22 +570,22 @@ public partial class PngEncoderTests switch (webpF.BlendMethod) { case WebpBlendMethod.Source: - Assert.Equal(PngBlendMethod.Source, pngF.BlendMethod); + Assert.Equal(FrameBlendMode.Source, pngF.BlendMode); break; case WebpBlendMethod.Over: default: - Assert.Equal(PngBlendMethod.Over, pngF.BlendMethod); + Assert.Equal(FrameBlendMode.Over, pngF.BlendMode); break; } switch (webpF.DisposalMethod) { case WebpDisposalMethod.RestoreToBackground: - Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, pngF.DisposalMode); break; case WebpDisposalMethod.DoNotDispose: default: - Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, pngF.DisposalMode); break; } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs index 9ba2617281..8fde216545 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Tests.Formats.Png; @@ -14,22 +15,22 @@ public class PngFrameMetadataTests PngFrameMetadata meta = new() { FrameDelay = new(1, 0), - DisposalMethod = PngDisposalMethod.RestoreToBackground, - BlendMethod = PngBlendMethod.Over, + DisposalMode = FrameDisposalMode.RestoreToBackground, + BlendMode = FrameBlendMode.Over, }; - PngFrameMetadata clone = (PngFrameMetadata)meta.DeepClone(); + PngFrameMetadata clone = meta.DeepClone(); Assert.True(meta.FrameDelay.Equals(clone.FrameDelay)); - Assert.True(meta.DisposalMethod.Equals(clone.DisposalMethod)); - Assert.True(meta.BlendMethod.Equals(clone.BlendMethod)); + Assert.True(meta.DisposalMode.Equals(clone.DisposalMode)); + Assert.True(meta.BlendMode.Equals(clone.BlendMode)); clone.FrameDelay = new(2, 1); - clone.DisposalMethod = PngDisposalMethod.RestoreToPrevious; - clone.BlendMethod = PngBlendMethod.Source; + clone.DisposalMode = FrameDisposalMode.RestoreToPrevious; + clone.BlendMode = FrameBlendMode.Source; Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); - Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); - Assert.False(meta.BlendMethod.Equals(clone.BlendMethod)); + Assert.False(meta.DisposalMode.Equals(clone.DisposalMode)); + Assert.False(meta.BlendMode.Equals(clone.BlendMode)); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 49c47adfd4..4d72363075 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -95,7 +95,7 @@ public class WebpEncoderTests Assert.Equal(gifF.FrameDelay, (int)(webpF.FrameDelay / 10)); - switch (gifF.DisposalMethod) + switch (gifF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); @@ -144,23 +144,23 @@ public class WebpEncoderTests Assert.Equal((uint)(pngF.FrameDelay.ToDouble() * 1000), webpF.FrameDelay); - switch (pngF.BlendMethod) + switch (pngF.BlendMode) { - case PngBlendMethod.Source: + case FrameBlendMode.Source: Assert.Equal(WebpBlendMethod.Source, webpF.BlendMethod); break; - case PngBlendMethod.Over: + case FrameBlendMode.Over: default: Assert.Equal(WebpBlendMethod.Over, webpF.BlendMethod); break; } - switch (pngF.DisposalMethod) + switch (pngF.DisposalMode) { - case PngDisposalMethod.RestoreToBackground: + case FrameDisposalMode.RestoreToBackground: Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); break; - case PngDisposalMethod.DoNotDispose: + case FrameDisposalMode.DoNotDispose: default: Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); break; diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index 572c5b2ebd..f70623f515 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -313,7 +313,7 @@ public abstract partial class ImageFrameCollectionTests GifFrameMetadata aData = a.Metadata.GetGifMetadata(); GifFrameMetadata bData = b.Metadata.GetGifMetadata(); - Assert.Equal(aData.DisposalMethod, bData.DisposalMethod); + Assert.Equal(aData.DisposalMode, bData.DisposalMode); Assert.Equal(aData.FrameDelay, bData.FrameDelay); if (aData.ColorTableMode == FrameColorTableMode.Local && bData.ColorTableMode == FrameColorTableMode.Local) diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index 9cb7137fe0..cdd6f0cc4f 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -28,14 +28,14 @@ public class ImageFrameMetadataTests GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); gifFrameMetadata.FrameDelay = frameDelay; gifFrameMetadata.LocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray(); - gifFrameMetadata.DisposalMethod = disposalMethod; + gifFrameMetadata.DisposalMode = disposalMethod; ImageFrameMetadata clone = new(metaData); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); Assert.Equal(colorTableLength, cloneGifFrameMetadata.LocalColorTable.Value.Length); - Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); + Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMode); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs index c427130054..05fd25cca9 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelColorTypeTests.cs @@ -49,6 +49,13 @@ public class PixelColorTypeTests Assert.True(colorType.HasFlag(PixelColorType.Luminance)); } + [Fact] + public void PixelColorType_Binary_ShouldBeSet() + { + const PixelColorType colorType = PixelColorType.Binary; + Assert.True(colorType.HasFlag(PixelColorType.Binary)); + } + [Fact] public void PixelColorType_Indexed_ShouldBeSet() { @@ -168,6 +175,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); @@ -185,6 +193,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); @@ -204,6 +213,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); @@ -222,6 +232,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); @@ -237,6 +248,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Blue)); Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.Cyan)); Assert.False(colorType.HasFlag(PixelColorType.Magenta)); @@ -244,6 +256,31 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Other)); } + [Fact] + public void PixelColorType_Binary_ShouldNotContainOtherFlags() + { + const PixelColorType colorType = PixelColorType.Binary; + Assert.False(colorType.HasFlag(PixelColorType.Red)); + Assert.False(colorType.HasFlag(PixelColorType.Green)); + Assert.False(colorType.HasFlag(PixelColorType.Blue)); + Assert.False(colorType.HasFlag(PixelColorType.Alpha)); + Assert.False(colorType.HasFlag(PixelColorType.Exponent)); + Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Indexed)); + Assert.False(colorType.HasFlag(PixelColorType.RGB)); + Assert.False(colorType.HasFlag(PixelColorType.BGR)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); + Assert.False(colorType.HasFlag(PixelColorType.ChrominanceRed)); + Assert.False(colorType.HasFlag(PixelColorType.YCbCr)); + Assert.False(colorType.HasFlag(PixelColorType.Cyan)); + Assert.False(colorType.HasFlag(PixelColorType.Magenta)); + Assert.False(colorType.HasFlag(PixelColorType.Yellow)); + Assert.False(colorType.HasFlag(PixelColorType.Key)); + Assert.False(colorType.HasFlag(PixelColorType.CMYK)); + Assert.False(colorType.HasFlag(PixelColorType.YCCK)); + Assert.False(colorType.HasFlag(PixelColorType.Other)); + } + [Fact] public void PixelColorType_Indexed_ShouldNotContainOtherFlags() { @@ -254,6 +291,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.RGB)); Assert.False(colorType.HasFlag(PixelColorType.BGR)); Assert.False(colorType.HasFlag(PixelColorType.ChrominanceBlue)); @@ -278,6 +316,7 @@ public class PixelColorTypeTests Assert.False(colorType.HasFlag(PixelColorType.Alpha)); Assert.False(colorType.HasFlag(PixelColorType.Exponent)); Assert.False(colorType.HasFlag(PixelColorType.Luminance)); + Assert.False(colorType.HasFlag(PixelColorType.Binary)); Assert.False(colorType.HasFlag(PixelColorType.Indexed)); Assert.False(colorType.HasFlag(PixelColorType.RGB)); Assert.False(colorType.HasFlag(PixelColorType.BGR)); From b9994b60db40d6fff53fcf116883baa15c1c742b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 28 May 2024 21:42:45 +1000 Subject: [PATCH 10/43] Implement QoiMetadata --- src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 8 +-- src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 56 +++++++++++++++++++- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index 86d81d8345..92974aade3 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -49,13 +49,7 @@ internal class QoiDecoderCore : IImageDecoderInternals this.ProcessHeader(stream); // Create Image object - ImageMetadata metadata = new() - { - DecodedImageFormat = QoiFormat.Instance, - HorizontalResolution = this.header.Width, - VerticalResolution = this.header.Height, - ResolutionUnits = PixelResolutionUnit.AspectRatio - }; + ImageMetadata metadata = new(); QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); qoiMetadata.Channels = this.header.Channels; qoiMetadata.ColorSpace = this.header.ColorSpace; diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index 610c6c15b8..4be714fc3c 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Qoi; /// /// Provides Qoi specific metadata information for the image. /// -public class QoiMetadata : IDeepCloneable +public class QoiMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -36,5 +38,55 @@ public class QoiMetadata : IDeepCloneable public QoiColorSpace ColorSpace { get; set; } /// - public IDeepCloneable DeepClone() => new QoiMetadata(this); + public static QoiMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PixelColorType color = metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB; + + if (color.HasFlag(PixelColorType.Alpha)) + { + return new QoiMetadata { Channels = QoiChannels.Rgba }; + } + + return new QoiMetadata { Channels = QoiChannels.Rgb }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + + switch (this.Channels) + { + case QoiChannels.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + default: + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new() + { + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + } + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public QoiMetadata DeepClone() => new(this); } From 1f4605ed3b8b78bed5e24ac43c205c6897e331cd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 May 2024 19:58:35 +1000 Subject: [PATCH 11/43] Implement TgaMetadata --- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 42 +++++------- src/ImageSharp/Formats/Tga/TgaMetadata.cs | 66 ++++++++++++++++++- .../Formats/Tga/TgaEncoderTests.cs | 1 + 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index bb13798c50..14351ee67c 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -3,8 +3,6 @@ using System.Buffers; using System.Buffers.Binary; -using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -160,19 +158,26 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals where TPixel : unmanaged, IPixel { Buffer2D pixels = image.PixelBuffer; + + using IMemoryOwner rgbaOwner = this.memoryAllocator.Allocate(image.Width); + Span rgbaRow = rgbaOwner.GetSpan(); + for (int y = 0; y < image.Height; y++) { Span pixelRow = pixels.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(image.Configuration, pixelRow, rgbaRow); + for (int x = 0; x < image.Width;) { TPixel currentPixel = pixelRow[x]; + Rgba32 rgba = rgbaRow[x]; byte equalPixelCount = FindEqualPixels(pixelRow, x); if (equalPixelCount > 0) { - // Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. + // Write the number of equal pixels, with the high bit set, indicating it's a compressed pixel run. stream.WriteByte((byte)(equalPixelCount | 128)); - this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); + this.WritePixel(stream, rgba); x += equalPixelCount + 1; } else @@ -180,12 +185,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals // Write Raw Packet (i.e., Non-Run-Length Encoded): byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x); stream.WriteByte(unEqualPixelCount); - this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); + this.WritePixel(stream, rgba); x++; for (int i = 0; i < unEqualPixelCount; i++) { currentPixel = pixelRow[x]; - this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); + rgba = rgbaRow[x]; + this.WritePixel(stream, rgba); x++; } } @@ -196,22 +202,19 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals /// /// Writes a the pixel to the stream. /// - /// The type of the pixel. /// The stream to write to. - /// The current pixel. /// The color of the pixel to write. - private void WritePixel(Stream stream, TPixel currentPixel, Rgba32 color) - where TPixel : unmanaged, IPixel + private void WritePixel(Stream stream, Rgba32 color) { switch (this.bitsPerPixel) { case TgaBitsPerPixel.Pixel8: - int luminance = GetLuminance(currentPixel); - stream.WriteByte((byte)luminance); + L8 l8 = L8.FromRgba32(color); + stream.WriteByte(l8.PackedValue); break; case TgaBitsPerPixel.Pixel16: - Bgra5551 bgra5551 = new(color.ToVector4()); + Bgra5551 bgra5551 = Bgra5551.FromRgba32(color); Span buffer = stackalloc byte[2]; BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue); stream.WriteByte(buffer[0]); @@ -402,17 +405,4 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals stream.Write(rowSpan); } } - - /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. - /// - /// The type of pixel format. - /// The pixel to get the luminance from. - [MethodImpl(InliningOptions.ShortMethod)] - public static int GetLuminance(TPixel sourcePixel) - where TPixel : unmanaged, IPixel - { - Vector4 vector = sourcePixel.ToVector4(); - return ColorNumerics.GetBT709Luminance(ref vector, 256); - } } diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 1fb3ab5c5d..345d1698e8 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Tga; /// /// Provides TGA specific metadata information for the image. /// -public class TgaMetadata : IDeepCloneable +public class TgaMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -33,5 +35,65 @@ public class TgaMetadata : IDeepCloneable public byte AlphaChannelBits { get; set; } /// - public IDeepCloneable DeepClone() => new TgaMetadata(this); + public static TgaMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + // TODO: AlphaChannelBits is not used during encoding. + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + return bpp switch + { + <= 8 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel8 }, + <= 16 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel16 }, + <= 24 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel24 }, + _ => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel32 } + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + int bpp = (int)this.BitsPerPixel; + + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha; + switch (this.BitsPerPixel) + { + case TgaBitsPerPixel.Pixel8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Luminance; + alpha = PixelAlphaRepresentation.None; + break; + case TgaBitsPerPixel.Pixel16: + info = PixelComponentInfo.Create(1, bpp, 5, 5, 5, 1); + color = PixelColorType.BGR | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + case TgaBitsPerPixel.Pixel24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + alpha = PixelAlphaRepresentation.None; + break; + case TgaBitsPerPixel.Pixel32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new() + { + PixelTypeInfo = new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + } + }; + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TgaMetadata DeepClone() => new(this); } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 709a3207aa..a3fa082c62 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -175,6 +175,7 @@ public class TgaEncoderTests using (var memStream = new MemoryStream()) { + image.DebugSave(provider, encoder); image.Save(memStream, encoder); memStream.Position = 0; using (var encodedImage = (Image)Image.Load(memStream)) From be0be6933fa05ffc45b67de7e7ea205a0a80bb4c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 May 2024 19:58:51 +1000 Subject: [PATCH 12/43] Fix PngMetadata --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 77 ++----------------- src/ImageSharp/Formats/Png/PngMetadata.cs | 31 ++++---- .../Formats/Png/PngEncoderTests.cs | 43 ----------- .../Formats/Png/PngSmokeTests.cs | 12 +-- 4 files changed, 28 insertions(+), 135 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 49157272c9..d1528d08b4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1484,41 +1484,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // Use options, then check metadata, if nothing set there then we suggest // a sensible default based upon the pixel format. - PngColorType? colorType = encoder.ColorType ?? pngMetadata.ColorType; - byte? bits = (byte?)(encoder.BitDepth ?? pngMetadata.BitDepth); + PngColorType color = encoder.ColorType ?? pngMetadata.ColorType; + byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth); - if (colorType is null || bits is null) - { - PixelTypeInfo info = TPixel.GetPixelTypeInfo(); - PixelComponentInfo? componentInfo = info.ComponentInfo; - - colorType ??= SuggestColorType(in info); - - if (bits is null) - { - // TODO: Update once we stop abusing PixelTypeInfo in decoders. - if (componentInfo.HasValue) - { - PixelComponentInfo c = componentInfo.Value; - bits = (byte)SuggestBitDepth(in c); - } - else - { - bits = (byte)PngBitDepth.Bit8; - } - } - } - - // Ensure bit depth and color type are a supported combination. + // Ensure the bit depth and color type are a supported combination. // Bit8 is the only bit depth supported by all color types. - byte[] validBitDepths = PngConstants.ColorTypes[colorType.Value]; + byte[] validBitDepths = PngConstants.ColorTypes[color]; if (Array.IndexOf(validBitDepths, bits) == -1) { bits = (byte)PngBitDepth.Bit8; } - this.colorType = colorType.Value; - this.bitDepth = bits.Value; + this.colorType = color; + this.bitDepth = bits; if (!encoder.FilterMethod.HasValue) { @@ -1529,7 +1507,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable use16Bit = bits == (byte)PngBitDepth.Bit16; bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit); - this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value; + this.interlaceMode = encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod; this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None; } @@ -1669,47 +1647,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable _ => use16Bit ? 8 : 4, }; - /// - /// Returns a suggested for the given - /// - /// The pixel type info. - /// The type of pixel format. - private static PngColorType SuggestColorType(in PixelTypeInfo info) - where TPixel : unmanaged, IPixel - { - if (info.AlphaRepresentation == PixelAlphaRepresentation.None) - { - return info.ColorType switch - { - PixelColorType.Luminance => PngColorType.Grayscale, - _ => PngColorType.Rgb, - }; - } - - return info.ColorType switch - { - PixelColorType.Luminance | PixelColorType.Alpha or PixelColorType.Alpha => PngColorType.GrayscaleWithAlpha, - _ => PngColorType.RgbWithAlpha, - }; - } - - /// - /// Returns a suggested for the given - /// - /// The pixel type info. - /// The type of pixel format. - private static PngBitDepth SuggestBitDepth(in PixelComponentInfo info) - where TPixel : unmanaged, IPixel - { - int bits = info.GetMaximumComponentPrecision(); - if (bits > (int)PixelComponentBitDepth.Bit8) - { - return PngBitDepth.Bit16; - } - - return PngBitDepth.Bit8; - } - private unsafe struct ScratchBuffer { private const int Size = 26; diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 49799f54b2..d789ae8fa3 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -47,17 +47,17 @@ public class PngMetadata : IFormatMetadata /// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. /// - public PngBitDepth? BitDepth { get; set; } + public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; /// /// Gets or sets the color type. /// - public PngColorType? ColorType { get; set; } + public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; /// /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image. /// - public PngInterlaceMode? InterlaceMethod { get; set; } = PngInterlaceMode.None; + public PngInterlaceMode InterlaceMethod { get; set; } = PngInterlaceMode.None; /// /// Gets or sets the gamma value for the image. @@ -100,21 +100,23 @@ public class PngMetadata : IFormatMetadata for (int i = 0; i < colorTable.Length; i++) { ref Color c = ref colorTable[i]; - if (c == metadata.BackgroundColor) + if (c != metadata.BackgroundColor) { - // Png treats background as fully empty - c = Color.Transparent; - break; + continue; } + + // Png treats background as fully empty + c = Color.Transparent; + break; } } return new() { - ColorType = colorTable != null ? PngColorType.Palette : null, + ColorType = colorTable != null ? PngColorType.Palette : PngColorType.RgbWithAlpha, BitDepth = colorTable != null ? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8) - : null, + : PngBitDepth.Bit8, ColorTable = colorTable, RepeatCount = metadata.RepeatCount, }; @@ -131,12 +133,14 @@ public class PngMetadata : IFormatMetadata for (int i = 0; i < colorTable.Length; i++) { ref Color c = ref colorTable[i]; - if (c == metadata.BackgroundColor) + if (c != metadata.BackgroundColor) { - // Png treats background as fully empty - c = Color.Transparent; - break; + continue; } + + // Png treats background as fully empty + c = Color.Transparent; + break; } } @@ -240,6 +244,7 @@ public class PngMetadata : IFormatMetadata info = PixelComponentInfo.Create(3, bpp, 16, 16, 16); break; + case PngColorType.RgbWithAlpha: default: alpha = PixelAlphaRepresentation.Unassociated; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index e61475b659..f40e537a15 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -254,49 +254,6 @@ public partial class PngEncoderTests } } - [Theory] - [WithBlankImages(1, 1, PixelTypes.A8, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Argb32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Bgr565, PngColorType.Rgb, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Bgra4444, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Byte4, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.HalfSingle, PngColorType.Rgb, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.HalfVector2, PngColorType.Rgb, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.HalfVector4, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.NormalizedByte2, PngColorType.Rgb, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.NormalizedByte4, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.NormalizedShort4, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Rg32, PngColorType.Rgb, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Rgba1010102, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.RgbaVector, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Short2, PngColorType.Rgb, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Short4, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Rgb24, PngColorType.Rgb, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Bgr24, PngColorType.Rgb, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Bgra32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.Rgb48, PngColorType.Rgb, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.Bgra5551, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.L8, PngColorType.Grayscale, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.L16, PngColorType.Grayscale, PngBitDepth.Bit16)] - [WithBlankImages(1, 1, PixelTypes.La16, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)] - [WithBlankImages(1, 1, PixelTypes.La32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)] - public void InfersColorTypeAndBitDepth(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) - where TPixel : unmanaged, IPixel - { - using Stream stream = new MemoryStream(); - PngEncoder.Encode(provider.GetImage(), stream); - - stream.Seek(0, SeekOrigin.Begin); - - using Image image = PngDecoder.Instance.Decode(DecoderOptions.Default, stream); - - PngMetadata metadata = image.Metadata.GetPngMetadata(); - Assert.Equal(pngColorType, metadata.ColorType); - Assert.Equal(pngBitDepth, metadata.BitDepth); - } - [Theory] [WithFile(TestImages.Png.Palette8Bpp, nameof(PaletteLargeOnly), PixelTypes.Rgba32)] public void PaletteColorType_WuQuantizer(TestImageProvider provider, int paletteSize) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs index a4288a3d84..3ca7730eb1 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs @@ -17,17 +17,13 @@ public class PngSmokeTests public void GeneralTest(TestImageProvider provider) where TPixel : unmanaged, IPixel { - // does saving a file then reopening mean both files are identical??? using Image image = provider.GetImage(); - using var ms = new MemoryStream(); + using MemoryStream ms = new(); - // image.Save(provider.Utility.GetTestOutputFileName("bmp")); image.Save(ms, new PngEncoder()); ms.Position = 0; using Image img2 = PngDecoder.Instance.Decode(DecoderOptions.Default, ms); - ImageComparer.Tolerant().VerifySimilarity(image, img2); - - // img2.Save(provider.Utility.GetTestOutputFileName("bmp", "_loaded"), new BmpEncoder()); + ImageComparer.Exact.VerifySimilarity(image, img2); } [Theory] @@ -37,12 +33,10 @@ public class PngSmokeTests { // does saving a file then reopening mean both files are identical??? using Image image = provider.GetImage(); - using var ms = new MemoryStream(); + using MemoryStream ms = new(); - // image.Save(provider.Utility.GetTestOutputFileName("png")); image.Mutate(x => x.Resize(100, 100)); - // image.Save(provider.Utility.GetTestOutputFileName("png", "resize")); image.Save(ms, new PngEncoder()); ms.Position = 0; using Image img2 = PngDecoder.Instance.Decode(DecoderOptions.Default, ms); From 4f6bd17120dcf60e7b66daa9bf3466a24d5ee499 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 May 2024 19:59:14 +1000 Subject: [PATCH 13/43] Fix PngMetadata --- src/ImageSharp/Formats/Png/PngDecoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 4a7ba3f961..e62a14b5e0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -53,8 +53,8 @@ public sealed class PngDecoder : SpecializedImageDecoder stream.Position = 0; PngMetadata meta = info.Metadata.GetPngMetadata(); - PngColorType color = meta.ColorType.GetValueOrDefault(); - PngBitDepth bits = meta.BitDepth.GetValueOrDefault(); + PngColorType color = meta.ColorType; + PngBitDepth bits = meta.BitDepth; switch (color) { From 7103e28d9002071c1fb46007192c3725bbcd1564 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 May 2024 19:59:21 +1000 Subject: [PATCH 14/43] Cleanup --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 100 +++++++++++----------- src/ImageSharp/Formats/Gif/GifMetadata.cs | 16 ++-- src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 15 ++-- 3 files changed, 66 insertions(+), 65 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 00c5910d4b..bb6b87e585 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -42,37 +42,25 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel1 }, + 2 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel2 }, + <= 4 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel4 }, + <= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 }, + <= 16 => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Pixel16, InfoHeaderType = BmpInfoHeaderType.WinVersion3 + }, + <= 24 => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.WinVersion4 + }, + _ => new BmpMetadata + { + BitsPerPixel = BmpBitsPerPixel.Pixel32, InfoHeaderType = BmpInfoHeaderType.WinVersion5 + } + }; } /// @@ -97,30 +85,42 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata bpp < 32 ? PixelAlphaRepresentation.None : PixelAlphaRepresentation.Unassociated }; - PixelComponentInfo info = this.BitsPerPixel switch + PixelComponentInfo info; + PixelColorType color; + switch (this.BitsPerPixel) { - BmpBitsPerPixel.Pixel1 => PixelComponentInfo.Create(1, bpp, 1), - BmpBitsPerPixel.Pixel2 => PixelComponentInfo.Create(1, bpp, 2), - BmpBitsPerPixel.Pixel4 => PixelComponentInfo.Create(1, bpp, 4), - BmpBitsPerPixel.Pixel8 => PixelComponentInfo.Create(1, bpp, 8), + case BmpBitsPerPixel.Pixel1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Pixel2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Pixel4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Pixel8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; // Could be 555 with padding but 565 is more common in newer bitmaps and offers // greater accuracy due to extra green precision. - BmpBitsPerPixel.Pixel16 => PixelComponentInfo.Create(3, bpp, 5, 6, 5), - BmpBitsPerPixel.Pixel24 => PixelComponentInfo.Create(3, bpp, 8, 8, 8), - BmpBitsPerPixel.Pixel32 or _ => PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8), - }; - - PixelColorType color = this.BitsPerPixel switch - { - BmpBitsPerPixel.Pixel1 or - BmpBitsPerPixel.Pixel2 or - BmpBitsPerPixel.Pixel4 or - BmpBitsPerPixel.Pixel8 => PixelColorType.Indexed, - BmpBitsPerPixel.Pixel16 or - BmpBitsPerPixel.Pixel24 => PixelColorType.RGB, - BmpBitsPerPixel.Pixel32 or _ => PixelColorType.RGB | PixelColorType.Alpha, - }; + case BmpBitsPerPixel.Pixel16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Pixel24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Pixel32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + break; + } return new() { diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 9a234aa9ba..64c7edbaf5 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -78,11 +78,13 @@ public class GifMetadata : IFormatMetadata ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; for (int i = 0; i < colorTable.Length; i++) { - if (background == colorTable[i]) + if (background != colorTable[i]) { - index = i; - break; + continue; } + + index = i; + break; } } @@ -105,11 +107,13 @@ public class GifMetadata : IFormatMetadata ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; for (int i = 0; i < colorTable.Length; i++) { - if (background == colorTable[i]) + if (background != colorTable[i]) { - index = i; - break; + continue; } + + index = i; + break; } } diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index 4e6b6f2650..49a13d5c33 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -40,7 +40,7 @@ public class PbmMetadata : IFormatMetadata /// /// Gets or sets the data type of the pixel components. /// - public PbmComponentType ComponentType { get; set; } = PbmComponentType.Byte; + public PbmComponentType ComponentType { get; set; } /// public static PbmMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) @@ -69,16 +69,13 @@ public class PbmMetadata : IFormatMetadata break; } - PbmComponentType componentType = PbmComponentType.Short; int bpp = metadata.PixelTypeInfo.BitsPerPixel; - if (bpp == 1) + PbmComponentType componentType = bpp switch { - componentType = PbmComponentType.Bit; - } - else if (bpp <= 8) - { - componentType = PbmComponentType.Byte; - } + 1 => PbmComponentType.Bit, + <= 8 => PbmComponentType.Byte, + _ => PbmComponentType.Short + }; return new PbmMetadata { From fdcd60200e904a05a1f34d76885a238d2f1e4471 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 5 Jun 2024 22:04:16 +1000 Subject: [PATCH 15/43] Begin migrating TiffMetadata --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 29 ++- src/ImageSharp/Formats/Gif/GifMetadata.cs | 24 +- src/ImageSharp/Formats/IFormatMetadata.cs | 8 + src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 22 +- src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 20 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 22 +- src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 20 +- src/ImageSharp/Formats/Tga/TgaMetadata.cs | 21 +- .../Formats/Tiff/Constants/TiffConstants.cs | 45 +++- .../Formats/Tiff/Ifd/EntryReader.cs | 2 +- .../Formats/Tiff/TiffBitsPerSample.cs | 8 +- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 78 +----- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 2 +- .../Formats/Tiff/TiffEncoderCore.cs | 239 ++++++------------ .../Formats/Tiff/TiffFrameMetadata.cs | 13 +- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 18 +- .../Formats/Tiff/TiffEncoderBaseTester.cs | 2 +- .../Formats/Tiff/TiffEncoderHeaderTests.cs | 4 +- .../Formats/Tiff/TiffEncoderTests.cs | 36 +-- .../TestUtilities/TestImageExtensions.cs | 17 -- 20 files changed, 267 insertions(+), 363 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index bb6b87e585..d44520a4f3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -50,15 +50,18 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 }, <= 16 => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel16, InfoHeaderType = BmpInfoHeaderType.WinVersion3 + BitsPerPixel = BmpBitsPerPixel.Pixel16, + InfoHeaderType = BmpInfoHeaderType.WinVersion3 }, <= 24 => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.WinVersion4 + BitsPerPixel = BmpBitsPerPixel.Pixel24, + InfoHeaderType = BmpInfoHeaderType.WinVersion4 }, _ => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel32, InfoHeaderType = BmpInfoHeaderType.WinVersion5 + BitsPerPixel = BmpBitsPerPixel.Pixel32, + InfoHeaderType = BmpInfoHeaderType.WinVersion5 } }; } @@ -68,7 +71,7 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata new(); /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp = (int)this.BitsPerPixel; @@ -122,17 +125,21 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new(); diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 64c7edbaf5..90d3312c85 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -126,6 +126,20 @@ public class GifMetadata : IFormatMetadata }; } + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = this.GlobalColorTable.HasValue + ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8) + : 8; + + return new PixelTypeInfo(bpp) + { + ColorType = PixelColorType.Indexed, + ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp), + }; + } + /// public FormatConnectingMetadata ToFormatConnectingMetadata() { @@ -133,21 +147,13 @@ public class GifMetadata : IFormatMetadata ? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex] : Color.Transparent; - int bpp = this.GlobalColorTable.HasValue - ? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8) - : 8; - return new() { AnimateRootFrame = true, BackgroundColor = color, ColorTable = this.GlobalColorTable, ColorTableMode = this.ColorTableMode, - PixelTypeInfo = new PixelTypeInfo(bpp) - { - ColorType = PixelColorType.Indexed, - ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp), - }, + PixelTypeInfo = this.GetPixelTypeInfo(), RepeatCount = this.RepeatCount, }; } diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs index f542f92db3..e0c32e8b26 100644 --- a/src/ImageSharp/Formats/IFormatMetadata.cs +++ b/src/ImageSharp/Formats/IFormatMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats; /// @@ -8,6 +10,12 @@ namespace SixLabors.ImageSharp.Formats; /// public interface IFormatMetadata : IDeepCloneable { + /// + /// Converts the metadata to a instance. + /// + /// The pixel type info. + PixelTypeInfo GetPixelTypeInfo(); + /// /// Converts the metadata to a instance. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index b12ee524d9..d15abafde8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -139,7 +139,7 @@ public class JpegMetadata : IFormatMetadata } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp; PixelColorType colorType; @@ -173,18 +173,22 @@ public class JpegMetadata : IFormatMetadata break; } - return new FormatConnectingMetadata + return new PixelTypeInfo(bpp) { - PixelTypeInfo = new PixelTypeInfo(bpp) - { - AlphaRepresentation = PixelAlphaRepresentation.None, - ColorType = colorType, - ComponentInfo = info, - }, - Quality = this.Quality, + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, }; } + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + Quality = this.Quality, + }; + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index 49a13d5c33..49a36de41e 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -85,7 +85,7 @@ public class PbmMetadata : IFormatMetadata } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp; PixelColorType colorType; @@ -114,17 +114,21 @@ public class PbmMetadata : IFormatMetadata break; } - return new FormatConnectingMetadata + return new PixelTypeInfo(bpp) { - PixelTypeInfo = new PixelTypeInfo(bpp) - { - AlphaRepresentation = PixelAlphaRepresentation.None, - ColorType = colorType, - ComponentInfo = info, - }, + AlphaRepresentation = PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, }; } + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + }; + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index d789ae8fa3..d72dc51841 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -191,7 +191,7 @@ public class PngMetadata : IFormatMetadata } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp; PixelColorType colorType; @@ -262,19 +262,23 @@ public class PngMetadata : IFormatMetadata break; } - return new() + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() { ColorTable = this.ColorTable, ColorTableMode = FrameColorTableMode.Global, - PixelTypeInfo = new PixelTypeInfo(bpp) - { - AlphaRepresentation = alpha, - ColorType = colorType, - ComponentInfo = info, - }, + PixelTypeInfo = this.GetPixelTypeInfo(), RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), }; - } /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index 4be714fc3c..c8bcd97480 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -51,7 +51,7 @@ public class QoiMetadata : IFormatMetadata } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp; PixelColorType colorType; @@ -73,17 +73,21 @@ public class QoiMetadata : IFormatMetadata break; } - return new() + return new PixelTypeInfo(bpp) { - PixelTypeInfo = new PixelTypeInfo(bpp) - { - AlphaRepresentation = alpha, - ColorType = colorType, - ComponentInfo = info, - } + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, }; } + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 345d1698e8..fa8e8b6f4f 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -49,10 +49,9 @@ public class TgaMetadata : IFormatMetadata } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() + public PixelTypeInfo GetPixelTypeInfo() { int bpp = (int)this.BitsPerPixel; - PixelComponentInfo info; PixelColorType color; PixelAlphaRepresentation alpha; @@ -80,17 +79,21 @@ public class TgaMetadata : IFormatMetadata break; } - return new() + return new PixelTypeInfo(bpp) { - PixelTypeInfo = new PixelTypeInfo(bpp) - { - AlphaRepresentation = alpha, - ComponentInfo = info, - ColorType = color - } + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color }; } + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs index 978860910c..c24eee484b 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs @@ -39,9 +39,9 @@ internal static class TiffConstants public const ushort BigTiffHeaderMagicNumber = 43; /// - /// The big tiff bytesize of offsets value. + /// The big tiff byte size of offsets value. /// - public const ushort BigTiffBytesize = 8; + public const ushort BigTiffByteSize = 8; /// /// RowsPerStrip default value, which is effectively infinity. @@ -58,38 +58,63 @@ internal static class TiffConstants /// public const int DefaultStripSize = 8 * 1024; + /// + /// The default predictor is None. + /// + public const TiffPredictor DefaultPredictor = TiffPredictor.None; + + /// + /// The default bits per pixel is Bit24. + /// + public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24; + + /// + /// The default bits per sample for color images with 8 bits for each color channel. + /// + public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit; + + /// + /// The default compression is None. + /// + public const TiffCompression DefaultCompression = TiffCompression.None; + + /// + /// The default photometric interpretation is Rgb. + /// + public const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + /// /// The bits per sample for 1 bit bicolor images. /// - public static readonly TiffBitsPerSample BitsPerSample1Bit = new TiffBitsPerSample(1, 0, 0); + public static readonly TiffBitsPerSample BitsPerSample1Bit = new(1, 0, 0); /// /// The bits per sample for images with a 4 color palette. /// - public static readonly TiffBitsPerSample BitsPerSample4Bit = new TiffBitsPerSample(4, 0, 0); + public static readonly TiffBitsPerSample BitsPerSample4Bit = new(4, 0, 0); /// /// The bits per sample for 8 bit images. /// - public static readonly TiffBitsPerSample BitsPerSample8Bit = new TiffBitsPerSample(8, 0, 0); + public static readonly TiffBitsPerSample BitsPerSample8Bit = new(8, 0, 0); /// /// The bits per sample for 16-bit grayscale images. /// - public static readonly TiffBitsPerSample BitsPerSample16Bit = new TiffBitsPerSample(16, 0, 0); + public static readonly TiffBitsPerSample BitsPerSample16Bit = new(16, 0, 0); /// /// The bits per sample for color images with 8 bits for each color channel. /// - public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new TiffBitsPerSample(8, 8, 8); + public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8); /// - /// The list of mimetypes that equate to a tiff. + /// The list of mime types that equate to a tiff. /// - public static readonly IEnumerable MimeTypes = new[] { "image/tiff", "image/tiff-fx" }; + public static readonly IEnumerable MimeTypes = ["image/tiff", "image/tiff-fx"]; /// /// The list of file extensions that equate to a tiff. /// - public static readonly IEnumerable FileExtensions = new[] { "tiff", "tif" }; + public static readonly IEnumerable FileExtensions = ["tiff", "tif"]; } diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs index 2673700092..4496de6fb5 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -64,7 +64,7 @@ internal class HeaderReader : BaseExifReader ushort bytesize = this.ReadUInt16(); ushort reserve = this.ReadUInt16(); - if (bytesize == TiffConstants.BigTiffBytesize && reserve == 0) + if (bytesize == TiffConstants.BigTiffByteSize && reserve == 0) { this.FirstIfdOffset = this.ReadUInt64(); return; diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs index 382faa3874..2bfd9a626f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs @@ -147,20 +147,20 @@ public readonly struct TiffBitsPerSample : IEquatable { if (this.Channel1 == 0) { - return new[] { this.Channel0 }; + return [this.Channel0]; } if (this.Channel2 == 0) { - return new[] { this.Channel0, this.Channel1 }; + return [this.Channel0, this.Channel1]; } if (this.Channel3 == 0) { - return new[] { this.Channel0, this.Channel1, this.Channel2 }; + return [this.Channel0, this.Channel1, this.Channel2]; } - return new[] { this.Channel0, this.Channel1, this.Channel2, this.Channel3 }; + return [this.Channel0, this.Channel1, this.Channel2, this.Channel3]; } /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 5a5c2b3e51..186a4bd314 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -63,7 +63,7 @@ internal static class TiffDecoderOptionsParser } TiffSampleFormat? sampleFormat = null; - if (exifProfile.TryGetValue(ExifTag.SampleFormat, out var formatValue)) + if (exifProfile.TryGetValue(ExifTag.SampleFormat, out IExifValue formatValue)) { TiffSampleFormat[] sampleFormats = formatValue.Value.Select(a => (TiffSampleFormat)a).ToArray(); sampleFormat = sampleFormats[0]; @@ -106,11 +106,11 @@ internal static class TiffDecoderOptionsParser options.PlanarConfiguration = DefaultPlanarConfiguration; } - options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None; - options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; + options.Predictor = frameMetadata.Predictor; + options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation; options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; - options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; - options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); + options.BitsPerPixel = (int)frameMetadata.BitsPerPixel; + options.BitsPerSample = frameMetadata.BitsPerSample; if (exifProfile.TryGetValue(ExifTag.ReferenceBlackWhite, out IExifValue blackWhiteValue)) { @@ -142,9 +142,7 @@ internal static class TiffDecoderOptionsParser options.ParseCompression(frameMetadata.Compression, exifProfile); options.ParseColorType(exifProfile); - bool isTiled = VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration); - - return isTiled; + return VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration); } /// @@ -194,13 +192,6 @@ internal static class TiffDecoderOptionsParser } } - // For BiColor compressed images, the BitsPerPixel value will be set explicitly to 1, so we don't throw in those cases. - // See: https://github.com/SixLabors/ImageSharp/issues/2587 - if (frameMetadata.BitsPerPixel == null && !IsBiColorCompression(frameMetadata.Compression)) - { - TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing which is required to decode the image!"); - } - return isTiled; } @@ -224,7 +215,6 @@ internal static class TiffDecoderOptionsParser switch (bitsPerChannel) { case 32: - { if (options.SampleFormat == TiffSampleFormat.Float) { options.ColorType = TiffColorType.WhiteIsZero32Float; @@ -233,43 +223,30 @@ internal static class TiffDecoderOptionsParser options.ColorType = TiffColorType.WhiteIsZero32; break; - } case 24: - { options.ColorType = TiffColorType.WhiteIsZero24; break; - } case 16: - { options.ColorType = TiffColorType.WhiteIsZero16; break; - } case 8: - { options.ColorType = TiffColorType.WhiteIsZero8; break; - } case 4: - { options.ColorType = TiffColorType.WhiteIsZero4; break; - } case 1: - { options.ColorType = TiffColorType.WhiteIsZero1; break; - } default: - { options.ColorType = TiffColorType.WhiteIsZero; break; - } } break; @@ -291,7 +268,6 @@ internal static class TiffDecoderOptionsParser switch (bitsPerChannel) { case 32: - { if (options.SampleFormat == TiffSampleFormat.Float) { options.ColorType = TiffColorType.BlackIsZero32Float; @@ -300,43 +276,30 @@ internal static class TiffDecoderOptionsParser options.ColorType = TiffColorType.BlackIsZero32; break; - } case 24: - { options.ColorType = TiffColorType.BlackIsZero24; break; - } case 16: - { options.ColorType = TiffColorType.BlackIsZero16; break; - } case 8: - { options.ColorType = TiffColorType.BlackIsZero8; break; - } case 4: - { options.ColorType = TiffColorType.BlackIsZero4; break; - } case 1: - { options.ColorType = TiffColorType.BlackIsZero1; break; - } default: - { options.ColorType = TiffColorType.BlackIsZero; break; - } } break; @@ -535,29 +498,21 @@ internal static class TiffDecoderOptionsParser switch (compression ?? TiffCompression.None) { case TiffCompression.None: - { options.CompressionType = TiffDecoderCompressionType.None; break; - } case TiffCompression.PackBits: - { options.CompressionType = TiffDecoderCompressionType.PackBits; break; - } case TiffCompression.Deflate: case TiffCompression.OldDeflate: - { options.CompressionType = TiffDecoderCompressionType.Deflate; break; - } case TiffCompression.Lzw: - { options.CompressionType = TiffDecoderCompressionType.Lzw; break; - } case TiffCompression.CcittGroup3Fax: { @@ -599,16 +554,13 @@ internal static class TiffDecoderOptionsParser } case TiffCompression.Ccitt1D: - { options.CompressionType = TiffDecoderCompressionType.HuffmanRle; options.BitsPerSample = new TiffBitsPerSample(1, 0, 0); options.BitsPerPixel = 1; break; - } case TiffCompression.OldJpeg: - { if (!options.OldJpegCompressionStartOfImageMarker.HasValue) { TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression"); @@ -629,10 +581,8 @@ internal static class TiffDecoderOptionsParser } break; - } case TiffCompression.Jpeg: - { options.CompressionType = TiffDecoderCompressionType.Jpeg; if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) @@ -643,30 +593,14 @@ internal static class TiffDecoderOptionsParser } break; - } case TiffCompression.Webp: - { options.CompressionType = TiffDecoderCompressionType.Webp; break; - } default: - { TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); break; - } } } - - private static bool IsBiColorCompression(TiffCompression? compression) - { - if (compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or - TiffCompression.CcittGroup4Fax) - { - return true; - } - - return false; - } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index ea64e82899..a068613bf4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -47,7 +47,7 @@ public class TiffEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - TiffEncoderCore encode = new(this, image.Configuration.MemoryAllocator); + TiffEncoderCore encode = new(this, image.Configuration); encode.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 149f23f1bf..c702252cc7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -1,8 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable -using SixLabors.ImageSharp.Advanced; +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Constants; @@ -50,41 +49,22 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals /// private readonly DeflateCompressionLevel compressionLevel; - /// - /// The default predictor is None. - /// - private const TiffPredictor DefaultPredictor = TiffPredictor.None; - - /// - /// The default bits per pixel is Bit24. - /// - private const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24; - - /// - /// The default compression is None. - /// - private const TiffCompression DefaultCompression = TiffCompression.None; - - /// - /// The default photometric interpretation is Rgb. - /// - private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - /// /// Whether to skip metadata during encoding. /// private readonly bool skipMetadata; - private readonly List<(long, uint)> frameMarkers = new(); + private readonly List<(long, uint)> frameMarkers = []; /// /// Initializes a new instance of the class. /// /// The options for the encoder. - /// The memory allocator. - public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator) + /// The global configuration. + public TiffEncoderCore(TiffEncoder options, Configuration configuration) { - this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; this.PhotometricInterpretation = options.PhotometricInterpretation; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.pixelSamplingStrategy = options.PixelSamplingStrategy; @@ -135,35 +115,29 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals // Determine the correct values to encode with. // EncoderOptions > Metadata > Default. - TiffBitsPerPixel? bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; + TiffBitsPerPixel bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; - TiffPhotometricInterpretation? photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; + TiffPhotometricInterpretation photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; - TiffPredictor predictor = - this.HorizontalPredictor - ?? rootFrameTiffMetaData.Predictor - ?? DefaultPredictor; + TiffPredictor predictor = this.HorizontalPredictor ?? rootFrameTiffMetaData.Predictor; - TiffCompression compression = - this.CompressionType - ?? rootFrameTiffMetaData.Compression - ?? DefaultCompression; + TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression; - // Make sure, the Encoder options makes sense in combination with each other. - this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); + // Make sure the Encoder options makes sense in combination with each other. + this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); using TiffStreamWriter writer = new(stream); Span buffer = stackalloc byte[4]; long ifdMarker = WriteHeader(writer, buffer); - Image metadataImage = image; + Image? metadataImage = image; foreach (ImageFrame frame in image.Frames) { cancellationToken.ThrowIfCancellationRequested(); - ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); + ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); metadataImage = null; } @@ -199,6 +173,8 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals /// The tiff frame. /// The image metadata (resolution values for each frame). /// The image (common metadata for root frame). + /// The bits per pixel. + /// The compression type. /// The marker to write this IFD offset. /// /// The next IFD offset value. @@ -207,16 +183,18 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals TiffStreamWriter writer, ImageFrame frame, ImageMetadata imageMetadata, - Image image, + Image? image, + TiffBitsPerPixel bitsPerPixel, + TiffCompression compression, long ifdOffset) where TPixel : unmanaged, IPixel { using TiffBaseCompressor compressor = TiffCompressorFactory.Create( - this.CompressionType ?? TiffCompression.None, + compression, writer.BaseStream, this.memoryAllocator, frame.Width, - (int)this.BitsPerPixel, + (int)bitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); @@ -229,7 +207,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals this.memoryAllocator, this.configuration, entriesCollector, - (int)this.BitsPerPixel); + (int)bitsPerPixel); int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); @@ -307,7 +285,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals } uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); - List largeDataBlocks = new(); + List largeDataBlocks = []; entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); @@ -354,135 +332,80 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals return nextIfdMarker; } + [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] private void SanitizeAndSetEncoderOptions( - TiffBitsPerPixel? bitsPerPixel, - int inputBitsPerPixel, - TiffPhotometricInterpretation? photometricInterpretation, + TiffBitsPerPixel bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression, TiffPredictor predictor) { // BitsPerPixel should be the primary source of truth for the encoder options. - if (bitsPerPixel.HasValue) - { - switch (bitsPerPixel) - { - case TiffBitsPerPixel.Bit1: - if (IsOneBitCompression(compression)) - { - // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); - break; - } - - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); - break; - case TiffBitsPerPixel.Bit4: - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); - break; - case TiffBitsPerPixel.Bit8: - this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor); - break; - case TiffBitsPerPixel.Bit16: - // Assume desire to encode as L16 grayscale - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); - break; - case TiffBitsPerPixel.Bit6: - case TiffBitsPerPixel.Bit10: - case TiffBitsPerPixel.Bit12: - case TiffBitsPerPixel.Bit14: - case TiffBitsPerPixel.Bit30: - case TiffBitsPerPixel.Bit36: - case TiffBitsPerPixel.Bit42: - case TiffBitsPerPixel.Bit48: - // Encoding not yet supported bits per pixel will default to 24 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); - break; - case TiffBitsPerPixel.Bit64: - // Encoding not yet supported bits per pixel will default to 32 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); - break; - default: - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); - break; - } - - // Make sure 1 Bit compression is only used with 1 bit pixel type. - if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1) - { - // Invalid compression / bits per pixel combination, fallback to no compression. - this.CompressionType = DefaultCompression; - } - - return; - } - - // If no photometric interpretation was chosen, the input image bit per pixel should be preserved. - if (!photometricInterpretation.HasValue) + switch (bitsPerPixel) { - if (IsOneBitCompression(this.CompressionType)) - { - // We need to make sure bits per pixel is set to Bit1 now. WhiteIsZero is set because its the default for bilevel compressed data. - this.SetEncoderOptions(TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); - return; - } - - // At the moment only 8, 16 and 32 bits per pixel can be preserved by the tiff encoder. - if (inputBitsPerPixel == 8) - { - this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); - return; - } - - if (inputBitsPerPixel == 16) - { - // Assume desire to encode as L16 grayscale - this.SetEncoderOptions(TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); - return; - } - - this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); - return; - } - - switch (photometricInterpretation) - { - case TiffPhotometricInterpretation.BlackIsZero: - case TiffPhotometricInterpretation.WhiteIsZero: - if (IsOneBitCompression(this.CompressionType)) - { - this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None); - return; - } - - if (inputBitsPerPixel == 16) + case TiffBitsPerPixel.Bit1: + if (IsOneBitCompression(compression)) { - this.SetEncoderOptions(TiffBitsPerPixel.Bit16, photometricInterpretation, compression, predictor); - return; + // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); + break; } - this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); - return; - - case TiffPhotometricInterpretation.PaletteColor: - this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); - return; - - case TiffPhotometricInterpretation.Rgb: - // Make sure 1 Bit compression is only used with 1 bit pixel type. - if (IsOneBitCompression(this.CompressionType)) + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); + break; + case TiffBitsPerPixel.Bit4: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); + break; + case TiffBitsPerPixel.Bit8: + + // Allow any combination of the below for 8 bit images. + if (photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero + or TiffPhotometricInterpretation.WhiteIsZero + or TiffPhotometricInterpretation.PaletteColor) { - // Invalid compression / bits per pixel combination, fallback to no compression. - compression = DefaultCompression; + this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); + break; } - this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor); - return; + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); + break; + case TiffBitsPerPixel.Bit16: + // Assume desire to encode as L16 grayscale + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); + break; + case TiffBitsPerPixel.Bit6: + case TiffBitsPerPixel.Bit10: + case TiffBitsPerPixel.Bit12: + case TiffBitsPerPixel.Bit14: + case TiffBitsPerPixel.Bit30: + case TiffBitsPerPixel.Bit36: + case TiffBitsPerPixel.Bit42: + case TiffBitsPerPixel.Bit48: + // Encoding not yet supported bits per pixel will default to 24 bits. + this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); + break; + case TiffBitsPerPixel.Bit64: + // Encoding not yet supported bits per pixel will default to 32 bits. + this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); + break; + default: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); + break; } - this.SetEncoderOptions(DefaultBitsPerPixel, DefaultPhotometricInterpretation, compression, predictor); + // Make sure 1 Bit compression is only used with 1 bit pixel type. + if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1) + { + // Invalid compression / bits per pixel combination, fallback to no compression. + this.CompressionType = TiffCompression.None; + } } - private void SetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) + [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] + private void SetEncoderOptions( + TiffBitsPerPixel bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + TiffPredictor predictor) { this.BitsPerPixel = bitsPerPixel; this.PhotometricInterpretation = photometricInterpretation; diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index e309830984..114fe703c1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -34,27 +34,27 @@ public class TiffFrameMetadata : IDeepCloneable /// /// Gets or sets the bits per pixel. /// - public TiffBitsPerPixel? BitsPerPixel { get; set; } + public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel; /// /// Gets or sets number of bits per component. /// - public TiffBitsPerSample? BitsPerSample { get; set; } + public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; /// /// Gets or sets the compression scheme used on the image data. /// - public TiffCompression? Compression { get; set; } + public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression; /// /// Gets or sets the color space of the image data. /// - public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation; /// /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. /// - public TiffPredictor? Predictor { get; set; } + public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor; /// /// Gets or sets the set of inks used in a separated () image. @@ -89,7 +89,7 @@ public class TiffFrameMetadata : IDeepCloneable meta.BitsPerSample = bitsPerSample; } - meta.BitsPerPixel = meta.BitsPerSample?.BitsPerPixel(); + meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) { @@ -111,6 +111,7 @@ public class TiffFrameMetadata : IDeepCloneable meta.InkSet = (TiffInkSet)inkSetValue.Value; } + // TODO: Why do we remove this? Encoding should overwrite. profile.RemoveValue(ExifTag.BitsPerSample); profile.RemoveValue(ExifTag.Compression); profile.RemoveValue(ExifTag.PhotometricInterpretation); diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index 2759d0130c..b95b9f92f1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Provides Tiff specific metadata information for the image. /// -public class TiffMetadata : IDeepCloneable +public class TiffMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -36,5 +38,17 @@ public class TiffMetadata : IDeepCloneable public TiffFormatType FormatType { get; set; } /// - public IDeepCloneable DeepClone() => new TiffMetadata(this); + public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + + /// + public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TiffMetadata DeepClone() => new(this); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs index 2a822e7054..0cff352171 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -30,7 +30,7 @@ public abstract class TiffEncoderBaseTester using Image input = provider.GetImage(); using var memStream = new MemoryStream(); TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); - TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; + TiffCompression inputCompression = inputMeta.Compression; // act input.Save(memStream, tiffEncoder); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index 8724147301..282966ea85 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -15,7 +15,7 @@ public class TiffEncoderHeaderTests public void WriteHeader_WritesValidHeader() { using MemoryStream stream = new(); - TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); + TiffEncoderCore encoder = new(Encoder, Configuration.Default); using (TiffStreamWriter writer = new(stream)) { @@ -29,7 +29,7 @@ public class TiffEncoderHeaderTests public void WriteHeader_ReturnsFirstIfdMarker() { using MemoryStream stream = new(); - TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); + TiffEncoderCore encoder = new(Encoder, Configuration.Default); using TiffStreamWriter writer = new(stream); long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 1fafb4cd04..1972101164 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -31,7 +31,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel) { // arrange - TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation }; + TiffEncoder tiffEncoder = new() { BitsPerPixel = expectedBitsPerPixel, PhotometricInterpretation = photometricInterpretation }; using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 ? new Image(10, 10) : new Image(10, 10); @@ -57,8 +57,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) { // arrange - TiffEncoder tiffEncoder = new() - { BitsPerPixel = bitsPerPixel }; + TiffEncoder tiffEncoder = new() { BitsPerPixel = bitsPerPixel }; using Image input = new Image(10, 10); using MemoryStream memStream = new(); @@ -156,7 +155,11 @@ public class TiffEncoderTests : TiffEncoderBaseTester { // arrange TiffEncoder tiffEncoder = new() - { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + { + BitsPerPixel = expectedBitsPerPixel, + PhotometricInterpretation = photometricInterpretation, + Compression = compression + }; using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 ? new Image(10, 10) : new Image(10, 10); @@ -199,25 +202,6 @@ public class TiffEncoderTests : TiffEncoderBaseTester Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); } - [Fact] - public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8() - { - // arrange - TiffEncoder tiffEncoder = new(); - using Image input = new Image(10, 10); - using MemoryStream memStream = new(); - const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8; - - // act - input.Save(memStream, tiffEncoder); - - // assert - memStream.Position = 0; - using Image output = Image.Load(memStream); - TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); - Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); - } - [Theory] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.None)] [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffCompression.Lzw)] @@ -241,11 +225,11 @@ public class TiffEncoderTests : TiffEncoderBaseTester } [Theory] - [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, null)] + [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffPredictor.None)] [WithFile(RgbLzwPredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] - [WithFile(RgbDeflate, PixelTypes.Rgba32, null)] + [WithFile(RgbDeflate, PixelTypes.Rgba32, TiffPredictor.None)] [WithFile(RgbDeflatePredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] - public void TiffEncoder_PreservesPredictor(TestImageProvider provider, TiffPredictor? expectedPredictor) + public void TiffEncoder_PreservesPredictor(TestImageProvider provider, TiffPredictor expectedPredictor) where TPixel : unmanaged, IPixel { // arrange diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 5da12f2643..05abedbd8e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -697,23 +697,6 @@ public static class TestImageExtensions return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf()); } - internal static Image ToGrayscaleImage(this Buffer2D buffer, float scale) - { - Image image = new(buffer.Width, buffer.Height); - - Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory pixelMem)); - Span pixels = pixelMem.Span; - Span bufferSpan = buffer.DangerousGetSingleSpan(); - - for (int i = 0; i < bufferSpan.Length; i++) - { - float value = bufferSpan[i] * scale; - pixels[i] = Rgba32.FromVector4(new Vector4(value, value, value, 1f)); - } - - return image; - } - private class MakeOpaqueProcessor : IImageProcessor { public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) From 1a9d578a04f0d48740d361eb33bec20256750267 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Jun 2024 21:31:43 +1000 Subject: [PATCH 16/43] Migrate TiffMetadata --- .../Tiff/TiffDecoderMetadataCreator.cs | 16 +- .../Formats/Tiff/TiffEncoderCore.cs | 35 +++-- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 140 +++++++++++++++++- 3 files changed, 172 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 1ef2478e3d..28565cac47 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -23,7 +23,7 @@ internal static class TiffDecoderMetadataCreator TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); } - ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].ExifProfile); + ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]); if (!ignoreMetadata) { @@ -50,14 +50,22 @@ internal static class TiffDecoderMetadataCreator return imageMetaData; } - private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ExifProfile exifProfile) + private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFrameMetadata rootFrameMetadata) { ImageMetadata imageMetaData = new(); - SetResolution(imageMetaData, exifProfile); + SetResolution(imageMetaData, rootFrameMetadata.ExifProfile); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); tiffMetadata.ByteOrder = byteOrder; tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default; + + TiffFrameMetadata tiffFrameMetadata = rootFrameMetadata.GetTiffMetadata(); + tiffMetadata.BitsPerPixel = tiffFrameMetadata.BitsPerPixel; + tiffMetadata.BitsPerSample = tiffFrameMetadata.BitsPerSample; + tiffMetadata.Compression = tiffFrameMetadata.Compression; + tiffMetadata.PhotometricInterpretation = tiffFrameMetadata.PhotometricInterpretation; + tiffMetadata.Predictor = tiffFrameMetadata.Predictor; + return imageMetaData; } @@ -109,7 +117,7 @@ internal static class TiffDecoderMetadataCreator return false; } - // Probably wrong endianess, swap byte order. + // Probably wrong endianness, swap byte order. Span iptcBytesSpan = iptcBytes.AsSpan(); Span buffer = stackalloc byte[4]; for (int i = 0; i < iptcBytes.Length; i += 4) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index c702252cc7..546ff7947d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -339,6 +339,20 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals TiffCompression compression, TiffPredictor predictor) { + // Ensure 1 Bit compression is only used with 1 bit pixel type. + // Choose a sensible default based on the bits per pixel. + if (IsOneBitCompression(compression) && bitsPerPixel != TiffBitsPerPixel.Bit1) + { + compression = bitsPerPixel switch + { + < TiffBitsPerPixel.Bit8 => TiffCompression.None, + _ => TiffCompression.Deflate, + }; + } + + // Ensure predictor is only used with compression that supports it. + predictor = HasPredictor(compression) ? predictor : TiffPredictor.None; + // BitsPerPixel should be the primary source of truth for the encoder options. switch (bitsPerPixel) { @@ -346,14 +360,14 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals if (IsOneBitCompression(compression)) { // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, predictor); break; } - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); break; case TiffBitsPerPixel.Bit4: - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); break; case TiffBitsPerPixel.Bit8: @@ -381,23 +395,17 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals case TiffBitsPerPixel.Bit42: case TiffBitsPerPixel.Bit48: // Encoding not yet supported bits per pixel will default to 24 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); + + this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); break; case TiffBitsPerPixel.Bit64: // Encoding not yet supported bits per pixel will default to 32 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); + this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, predictor); break; default: this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); break; } - - // Make sure 1 Bit compression is only used with 1 bit pixel type. - if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1) - { - // Invalid compression / bits per pixel combination, fallback to no compression. - this.CompressionType = TiffCompression.None; - } } [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] @@ -415,4 +423,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals public static bool IsOneBitCompression(TiffCompression? compression) => compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or TiffCompression.CcittGroup4Fax; + + public static bool HasPredictor(TiffCompression? compression) + => compression is TiffCompression.Deflate or TiffCompression.Lzw; } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index b95b9f92f1..cc70941d51 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -25,6 +26,11 @@ public class TiffMetadata : IFormatMetadata { this.ByteOrder = other.ByteOrder; this.FormatType = other.FormatType; + this.BitsPerPixel = other.BitsPerPixel; + this.BitsPerSample = other.BitsPerSample; + this.Compression = other.Compression; + this.PhotometricInterpretation = other.PhotometricInterpretation; + this.Predictor = other.Predictor; } /// @@ -37,14 +43,142 @@ public class TiffMetadata : IFormatMetadata /// public TiffFormatType FormatType { get; set; } + /// + /// Gets or sets the bits per pixel. Derived from the root frame. + /// + public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel; + + /// + /// Gets or sets number of bits per component. Derived from the root frame. + /// + public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + + /// + /// Gets or sets the compression scheme used on the image data. Derived from the root frame. + /// + public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression; + + /// + /// Gets or sets the color space of the image data. Derived from the root frame. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation; + + /// + /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. + /// Derived from the root frame. + /// + public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor; + /// - public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + return bpp switch + { + 1 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit1, + BitsPerSample = TiffConstants.BitsPerSample1Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero, + Compression = TiffCompression.CcittGroup4Fax, + Predictor = TiffPredictor.None + }, + <= 4 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit4, + BitsPerSample = TiffConstants.BitsPerSample4Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.None // Best match for low bit depth + }, + 8 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit8, + BitsPerSample = TiffConstants.BitsPerSample8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + 16 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit16, + BitsPerSample = TiffConstants.BitsPerSample16Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + 32 or 64 => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit32, + BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + }, + _ => new TiffMetadata + { + BitsPerPixel = TiffBitsPerPixel.Bit24, + BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit, + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + Compression = TiffCompression.Deflate, + Predictor = TiffPredictor.Horizontal + } + }; + } /// - public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BitsPerPixel; + + TiffBitsPerSample samples = this.BitsPerSample; + PixelComponentInfo info = samples.Channels switch + { + 1 => PixelComponentInfo.Create(1, bpp, bpp), + 2 => PixelComponentInfo.Create(2, bpp, bpp, samples.Channel0, samples.Channel1), + 3 => PixelComponentInfo.Create(3, bpp, samples.Channel0, samples.Channel1, samples.Channel2), + _ => PixelComponentInfo.Create(4, bpp, samples.Channel0, samples.Channel1, samples.Channel2, samples.Channel3) + }; + + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + switch (this.BitsPerPixel) + { + case TiffBitsPerPixel.Bit1: + colorType = PixelColorType.Binary; + break; + case TiffBitsPerPixel.Bit4: + case TiffBitsPerPixel.Bit6: + case TiffBitsPerPixel.Bit8: + colorType = PixelColorType.Indexed; + break; + case TiffBitsPerPixel.Bit16: + colorType = PixelColorType.Luminance; + break; + case TiffBitsPerPixel.Bit32: + case TiffBitsPerPixel.Bit64: + colorType = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + default: + colorType = PixelColorType.RGB; + break; + } + + return new PixelTypeInfo(bpp) + { + ColorType = colorType, + ComponentInfo = info, + AlphaRepresentation = alpha + }; + } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); From d371a62e1651cf0f834a5bae70ddfc867a53da06 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Jun 2024 22:32:46 +1000 Subject: [PATCH 17/43] Migrate TiffFrameMetadata --- .../Formats/Tiff/TiffFrameMetadata.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index 114fe703c1..bb5da37411 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Provides Tiff specific metadata information for the frame. /// -public class TiffFrameMetadata : IDeepCloneable +public class TiffFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -61,6 +61,20 @@ public class TiffFrameMetadata : IDeepCloneable /// public TiffInkSet? InkSet { get; set; } + /// + public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new(); + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new(); + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public TiffFrameMetadata DeepClone() => new(this); + /// /// Returns a new instance parsed from the given Exif profile. /// @@ -118,7 +132,4 @@ public class TiffFrameMetadata : IDeepCloneable profile.RemoveValue(ExifTag.Predictor); } } - - /// - public IDeepCloneable DeepClone() => new TiffFrameMetadata(this); } From fbefb37a1042a87315ed13fdaf2a73ee7baf85da Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jun 2024 17:25:13 +1000 Subject: [PATCH 18/43] Jpeg should not have nullable metadata --- .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 243bbe051d..b6fda8bc30 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -72,7 +72,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata); - bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved ?? true; + bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved; using JpegFrame frame = new(image, frameConfig, interleaved); // Write the Start Of Image marker. diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index d15abafde8..86b7883c38 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -48,11 +48,12 @@ public class JpegMetadata : IFormatMetadata internal int? ChrominanceQuality { get; set; } /// - /// Gets the encoded quality. + /// Gets or sets the encoded quality. /// /// /// Note that jpeg image can have different quality for luminance and chrominance components. /// This property returns maximum value of luma/chroma qualities if both are present. + /// Setting the quality will update both values. /// public int Quality { @@ -70,34 +71,40 @@ public class JpegMetadata : IFormatMetadata return this.ChrominanceQuality ?? Quantization.DefaultQualityFactor; } + + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } } /// - /// Gets the color type. + /// Gets or sets the color type. /// - public JpegEncodingColor? ColorType { get; internal set; } + public JpegEncodingColor ColorType { get; set; } = JpegEncodingColor.YCbCrRatio420; /// - /// Gets the component encoding mode. + /// Gets or sets a value indicating whether the component encoding mode should be interleaved. /// /// /// Interleaved encoding mode encodes all color components in a single scan. /// Non-interleaved encoding mode encodes each color component in a separate scan. /// - public bool? Interleaved { get; internal set; } + public bool Interleaved { get; set; } = true; /// - /// Gets the scan encoding mode. + /// Gets or sets a value indicating whether the scan encoding mode is progressive. /// /// /// Progressive jpeg images encode component data across multiple scans. /// - public bool? Progressive { get; internal set; } + public bool Progressive { get; set; } /// - /// Gets the comments. + /// Gets or sets collection of comments. /// - public IList Comments { get; } + public IList Comments { get; set; } = []; /// public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) From 3d80dfcbaca92112dab78c1219207b7bb21fe4c0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jun 2024 17:27:12 +1000 Subject: [PATCH 19/43] Rename to JpegColorType --- .../EncodingConfigs/JpegFrameConfig.cs | 4 +- .../Components/Encoder/HuffmanScanEncoder.cs | 6 +- ...{JpegEncodingColor.cs => JpegColorType.cs} | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 24 ++-- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 2 +- .../Jpeg/JpegEncoderCore.FrameConfig.cs | 18 +-- .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 24 ++-- .../Compressors/TiffJpegCompressor.cs | 2 +- .../Codecs/Jpeg/EncodeJpegComparison.cs | 2 +- .../Codecs/Jpeg/EncodeJpegFeatures.cs | 12 +- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 28 ++--- .../Formats/Jpg/JpegEncoderTests.Metadata.cs | 10 +- .../Formats/Jpg/JpegEncoderTests.cs | 112 +++++++++--------- .../Formats/Jpg/JpegMetadataTests.cs | 4 +- 15 files changed, 126 insertions(+), 126 deletions(-) rename src/ImageSharp/Formats/Jpeg/{JpegEncodingColor.cs => JpegColorType.cs} (98%) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs index 5a59837e58..0b7b21f90b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; internal class JpegFrameConfig { - public JpegFrameConfig(JpegColorSpace colorType, JpegEncodingColor encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables) + public JpegFrameConfig(JpegColorSpace colorType, JpegColorType encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables) { this.ColorType = colorType; this.EncodingColor = encodingColor; @@ -25,7 +25,7 @@ internal class JpegFrameConfig public JpegColorSpace ColorType { get; } - public JpegEncodingColor EncodingColor { get; } + public JpegColorType EncodingColor { get; } public JpegComponentConfig[] Components { get; } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index d74494f9e5..ac527ff312 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -139,13 +139,13 @@ internal class HuffmanScanEncoder /// Frame to encode. /// Converter from color to spectral. /// The token to request cancellation. - public void EncodeScanBaselineInterleaved(JpegEncodingColor color, JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + public void EncodeScanBaselineInterleaved(JpegColorType color, JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { switch (color) { - case JpegEncodingColor.YCbCrRatio444: - case JpegEncodingColor.Rgb: + case JpegColorType.YCbCrRatio444: + case JpegColorType.Rgb: this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken); break; default: diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs similarity index 98% rename from src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs rename to src/ImageSharp/Formats/Jpeg/JpegColorType.cs index 779ccf61e1..a8429273fe 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// /// Provides enumeration of available JPEG color types. /// -public enum JpegEncodingColor : byte +public enum JpegColorType : byte { /// /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 906505b76a..6028aab300 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -603,58 +603,58 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// Returns the jpeg color type based on the colorspace and subsampling used. /// /// Jpeg color type. - private JpegEncodingColor DeduceJpegColorType() + private JpegColorType DeduceJpegColorType() { switch (this.ColorSpace) { case JpegColorSpace.Grayscale: - return JpegEncodingColor.Luminance; + return JpegColorType.Luminance; case JpegColorSpace.RGB: - return JpegEncodingColor.Rgb; + return JpegColorType.Rgb; case JpegColorSpace.YCbCr: if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) { - return JpegEncodingColor.YCbCrRatio444; + return JpegColorType.YCbCrRatio444; } else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) { - return JpegEncodingColor.YCbCrRatio422; + return JpegColorType.YCbCrRatio422; } else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) { - return JpegEncodingColor.YCbCrRatio420; + return JpegColorType.YCbCrRatio420; } else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) { - return JpegEncodingColor.YCbCrRatio411; + return JpegColorType.YCbCrRatio411; } else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) { - return JpegEncodingColor.YCbCrRatio410; + return JpegColorType.YCbCrRatio410; } else { - return JpegEncodingColor.YCbCrRatio420; + return JpegColorType.YCbCrRatio420; } case JpegColorSpace.Cmyk: - return JpegEncodingColor.Cmyk; + return JpegColorType.Cmyk; case JpegColorSpace.Ycck: - return JpegEncodingColor.Ycck; + return JpegColorType.Ycck; default: - return JpegEncodingColor.YCbCrRatio420; + return JpegColorType.YCbCrRatio420; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 5ff4b1694d..0daaae112c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -45,7 +45,7 @@ public sealed class JpegEncoder : ImageEncoder /// /// Gets the jpeg color for encoding. /// - public JpegEncodingColor? ColorType { get; init; } + public JpegColorType? ColorType { get; init; } /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs index 4aed795825..71f852a092 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs @@ -40,7 +40,7 @@ internal sealed unsafe partial class JpegEncoderCore // YCbCr 4:4:4 new JpegFrameConfig( JpegColorSpace.YCbCr, - JpegEncodingColor.YCbCrRatio444, + JpegColorType.YCbCrRatio444, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -53,7 +53,7 @@ internal sealed unsafe partial class JpegEncoderCore // YCbCr 4:2:2 new JpegFrameConfig( JpegColorSpace.YCbCr, - JpegEncodingColor.YCbCrRatio422, + JpegColorType.YCbCrRatio422, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -66,7 +66,7 @@ internal sealed unsafe partial class JpegEncoderCore // YCbCr 4:2:0 new JpegFrameConfig( JpegColorSpace.YCbCr, - JpegEncodingColor.YCbCrRatio420, + JpegColorType.YCbCrRatio420, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -79,7 +79,7 @@ internal sealed unsafe partial class JpegEncoderCore // YCbCr 4:1:1 new JpegFrameConfig( JpegColorSpace.YCbCr, - JpegEncodingColor.YCbCrRatio411, + JpegColorType.YCbCrRatio411, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -92,7 +92,7 @@ internal sealed unsafe partial class JpegEncoderCore // YCbCr 4:1:0 new JpegFrameConfig( JpegColorSpace.YCbCr, - JpegEncodingColor.YCbCrRatio410, + JpegColorType.YCbCrRatio410, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -105,7 +105,7 @@ internal sealed unsafe partial class JpegEncoderCore // Luminance new JpegFrameConfig( JpegColorSpace.Grayscale, - JpegEncodingColor.Luminance, + JpegColorType.Luminance, new JpegComponentConfig[] { new JpegComponentConfig(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -123,7 +123,7 @@ internal sealed unsafe partial class JpegEncoderCore // Rgb new JpegFrameConfig( JpegColorSpace.RGB, - JpegEncodingColor.Rgb, + JpegColorType.Rgb, new JpegComponentConfig[] { new JpegComponentConfig(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -146,7 +146,7 @@ internal sealed unsafe partial class JpegEncoderCore // Cmyk new JpegFrameConfig( JpegColorSpace.Cmyk, - JpegEncodingColor.Cmyk, + JpegColorType.Cmyk, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), @@ -170,7 +170,7 @@ internal sealed unsafe partial class JpegEncoderCore // YccK new JpegFrameConfig( JpegColorSpace.Ycck, - JpegEncodingColor.Ycck, + JpegColorType.Ycck, new JpegComponentConfig[] { new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index b6fda8bc30..523d6b3ba8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -780,7 +780,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals private JpegFrameConfig GetFrameConfig(JpegMetadata metadata) { - JpegEncodingColor color = this.encoder.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420; + JpegColorType color = this.encoder.ColorType ?? metadata.ColorType; JpegFrameConfig frameConfig = Array.Find( FrameConfigs, cfg => cfg.EncodingColor == color); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 86b7883c38..0fadc830e5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -82,7 +82,7 @@ public class JpegMetadata : IFormatMetadata /// /// Gets or sets the color type. /// - public JpegEncodingColor ColorType { get; set; } = JpegEncodingColor.YCbCrRatio420; + public JpegColorType ColorType { get; set; } = JpegColorType.YCbCrRatio420; /// /// Gets or sets a value indicating whether the component encoding mode should be interleaved. @@ -109,29 +109,29 @@ public class JpegMetadata : IFormatMetadata /// public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { - JpegEncodingColor color; + JpegColorType color; PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.YCbCr; switch (colorType) { case PixelColorType.Luminance: - color = JpegEncodingColor.Luminance; + color = JpegColorType.Luminance; break; case PixelColorType.CMYK: - color = JpegEncodingColor.Cmyk; + color = JpegColorType.Cmyk; break; case PixelColorType.YCCK: - color = JpegEncodingColor.Ycck; + color = JpegColorType.Ycck; break; default: if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR)) { - color = JpegEncodingColor.Rgb; + color = JpegColorType.Rgb; } else { color = metadata.Quality <= Quantization.DefaultQualityFactor - ? JpegEncodingColor.YCbCrRatio420 - : JpegEncodingColor.YCbCrRatio444; + ? JpegColorType.YCbCrRatio420 + : JpegColorType.YCbCrRatio444; } break; @@ -153,22 +153,22 @@ public class JpegMetadata : IFormatMetadata PixelComponentInfo info; switch (this.ColorType) { - case JpegEncodingColor.Luminance: + case JpegColorType.Luminance: bpp = 8; colorType = PixelColorType.Luminance; info = PixelComponentInfo.Create(1, bpp, 8); break; - case JpegEncodingColor.Cmyk: + case JpegColorType.Cmyk: bpp = 32; colorType = PixelColorType.CMYK; info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); break; - case JpegEncodingColor.Ycck: + case JpegColorType.Ycck: bpp = 32; colorType = PixelColorType.YCCK; info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); break; - case JpegEncodingColor.Rgb: + case JpegColorType.Rgb: bpp = 24; colorType = PixelColorType.RGB; info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs index 9096271fe5..08faa539a8 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs @@ -33,7 +33,7 @@ internal class TiffJpegCompressor : TiffBaseCompressor var image = Image.LoadPixelData(rows, width, height); image.Save(memoryStream, new JpegEncoder() { - ColorType = JpegEncodingColor.Rgb + ColorType = JpegColorType.Rgb }); memoryStream.Position = 0; memoryStream.WriteTo(this.Output); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs index d762e8e95e..deb3125b30 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs @@ -39,7 +39,7 @@ public class EncodeJpegComparison using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); this.imageImageSharp = Image.Load(imageBinaryStream); - this.encoderImageSharp = new JpegEncoder { Quality = this.Quality, ColorType = JpegEncodingColor.YCbCrRatio420 }; + this.encoderImageSharp = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 }; this.destinationStream = new MemoryStream(); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs index 98eb0b54dc..0692c5a3b5 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs @@ -20,19 +20,19 @@ public class EncodeJpegFeatures // No metadata private const string TestImage = TestImages.Jpeg.Baseline.Calliphora; - public static IEnumerable ColorSpaceValues => new[] + public static IEnumerable ColorSpaceValues => new[] { - JpegEncodingColor.Luminance, - JpegEncodingColor.Rgb, - JpegEncodingColor.YCbCrRatio420, - JpegEncodingColor.YCbCrRatio444, + JpegColorType.Luminance, + JpegColorType.Rgb, + JpegColorType.YCbCrRatio420, + JpegColorType.YCbCrRatio444, }; [Params(75, 90, 100)] public int Quality; [ParamsSource(nameof(ColorSpaceValues), Priority = -100)] - public JpegEncodingColor TargetColorSpace; + public JpegColorType TargetColorSpace; private Image bmpCore; private JpegEncoder encoder; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index cbb2befcd4..a2c144a656 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -145,14 +145,14 @@ public partial class JpegDecoderTests } [Theory] - [InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegEncodingColor.Luminance)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegEncodingColor.YCbCrRatio420)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegEncodingColor.YCbCrRatio444)] - [InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegEncodingColor.Rgb)] - [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegEncodingColor.Cmyk)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegEncodingColor.YCbCrRatio410)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegEncodingColor.YCbCrRatio411)] - public void Identify_DetectsCorrectColorType(string imagePath, JpegEncodingColor expectedColorType) + [InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegColorType.Luminance)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegColorType.YCbCrRatio420)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegColorType.YCbCrRatio444)] + [InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegColorType.Rgb)] + [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorType.Cmyk)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegColorType.YCbCrRatio410)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegColorType.YCbCrRatio411)] + public void Identify_DetectsCorrectColorType(string imagePath, JpegColorType expectedColorType) { TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); @@ -162,12 +162,12 @@ public partial class JpegDecoderTests } [Theory] - [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)] - [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)] - [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)] - [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegEncodingColor.Rgb)] - [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgb24, JpegEncodingColor.Cmyk)] - public void Decode_DetectsCorrectColorType(TestImageProvider provider, JpegEncodingColor expectedColorType) + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegColorType.Luminance)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegColorType.YCbCrRatio420)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegColorType.YCbCrRatio444)] + [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegColorType.Rgb)] + [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgb24, JpegColorType.Cmyk)] + public void Decode_DetectsCorrectColorType(TestImageProvider provider, JpegColorType expectedColorType) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(JpegDecoder.Instance); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs index f06fbe9635..039215bbc5 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs @@ -208,11 +208,11 @@ public partial class JpegEncoderTests } [Theory] - [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)] - [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)] - [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)] - [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegEncodingColor.Rgb)] - public void Encode_PreservesColorType(TestImageProvider provider, JpegEncodingColor expectedColorType) + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegColorType.Luminance)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegColorType.YCbCrRatio444)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegColorType.YCbCrRatio420)] + [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegColorType.Rgb)] + public void Encode_PreservesColorType(TestImageProvider provider, JpegColorType expectedColorType) where TPixel : unmanaged, IPixel { // arrange diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 5842c8e1a0..1f4b3e4656 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -21,51 +21,51 @@ public partial class JpegEncoderTests 100, }; - public static readonly TheoryData NonSubsampledEncodingSetups = new() + public static readonly TheoryData NonSubsampledEncodingSetups = new() { - { JpegEncodingColor.Rgb, 100, 0.0238f / 100 }, - { JpegEncodingColor.Rgb, 80, 1.3044f / 100 }, - { JpegEncodingColor.Rgb, 40, 2.9879f / 100 }, - { JpegEncodingColor.YCbCrRatio444, 100, 0.0780f / 100 }, - { JpegEncodingColor.YCbCrRatio444, 80, 1.4585f / 100 }, - { JpegEncodingColor.YCbCrRatio444, 40, 3.1413f / 100 }, + { JpegColorType.Rgb, 100, 0.0238f / 100 }, + { JpegColorType.Rgb, 80, 1.3044f / 100 }, + { JpegColorType.Rgb, 40, 2.9879f / 100 }, + { JpegColorType.YCbCrRatio444, 100, 0.0780f / 100 }, + { JpegColorType.YCbCrRatio444, 80, 1.4585f / 100 }, + { JpegColorType.YCbCrRatio444, 40, 3.1413f / 100 }, }; - public static readonly TheoryData SubsampledEncodingSetups = new() + public static readonly TheoryData SubsampledEncodingSetups = new() { - { JpegEncodingColor.YCbCrRatio422, 100, 0.4895f / 100 }, - { JpegEncodingColor.YCbCrRatio422, 80, 1.6043f / 100 }, - { JpegEncodingColor.YCbCrRatio422, 40, 3.1996f / 100 }, - { JpegEncodingColor.YCbCrRatio420, 100, 0.5790f / 100 }, - { JpegEncodingColor.YCbCrRatio420, 80, 1.6692f / 100 }, - { JpegEncodingColor.YCbCrRatio420, 40, 3.2324f / 100 }, - { JpegEncodingColor.YCbCrRatio411, 100, 0.6868f / 100 }, - { JpegEncodingColor.YCbCrRatio411, 80, 1.7139f / 100 }, - { JpegEncodingColor.YCbCrRatio411, 40, 3.2634f / 100 }, - { JpegEncodingColor.YCbCrRatio410, 100, 0.7357f / 100 }, - { JpegEncodingColor.YCbCrRatio410, 80, 1.7495f / 100 }, - { JpegEncodingColor.YCbCrRatio410, 40, 3.2911f / 100 }, + { JpegColorType.YCbCrRatio422, 100, 0.4895f / 100 }, + { JpegColorType.YCbCrRatio422, 80, 1.6043f / 100 }, + { JpegColorType.YCbCrRatio422, 40, 3.1996f / 100 }, + { JpegColorType.YCbCrRatio420, 100, 0.5790f / 100 }, + { JpegColorType.YCbCrRatio420, 80, 1.6692f / 100 }, + { JpegColorType.YCbCrRatio420, 40, 3.2324f / 100 }, + { JpegColorType.YCbCrRatio411, 100, 0.6868f / 100 }, + { JpegColorType.YCbCrRatio411, 80, 1.7139f / 100 }, + { JpegColorType.YCbCrRatio411, 40, 3.2634f / 100 }, + { JpegColorType.YCbCrRatio410, 100, 0.7357f / 100 }, + { JpegColorType.YCbCrRatio410, 80, 1.7495f / 100 }, + { JpegColorType.YCbCrRatio410, 40, 3.2911f / 100 }, }; - public static readonly TheoryData CmykEncodingSetups = new() + public static readonly TheoryData CmykEncodingSetups = new() { - { JpegEncodingColor.Cmyk, 100, 0.0159f / 100 }, - { JpegEncodingColor.Cmyk, 80, 0.3922f / 100 }, - { JpegEncodingColor.Cmyk, 40, 0.6488f / 100 }, + { JpegColorType.Cmyk, 100, 0.0159f / 100 }, + { JpegColorType.Cmyk, 80, 0.3922f / 100 }, + { JpegColorType.Cmyk, 40, 0.6488f / 100 }, }; - public static readonly TheoryData YcckEncodingSetups = new() + public static readonly TheoryData YcckEncodingSetups = new() { - { JpegEncodingColor.Ycck, 100, 0.0356f / 100 }, - { JpegEncodingColor.Ycck, 80, 0.1245f / 100 }, - { JpegEncodingColor.Ycck, 40, 0.2663f / 100 }, + { JpegColorType.Ycck, 100, 0.0356f / 100 }, + { JpegColorType.Ycck, 80, 0.1245f / 100 }, + { JpegColorType.Ycck, 40, 0.2663f / 100 }, }; - public static readonly TheoryData LuminanceEncodingSetups = new() + public static readonly TheoryData LuminanceEncodingSetups = new() { - { JpegEncodingColor.Luminance, 100, 0.0175f / 100 }, - { JpegEncodingColor.Luminance, 80, 0.6730f / 100 }, - { JpegEncodingColor.Luminance, 40, 0.9943f / 100 }, + { JpegColorType.Luminance, 100, 0.0175f / 100 }, + { JpegColorType.Luminance, 80, 0.6730f / 100 }, + { JpegColorType.Luminance, 40, 0.9943f / 100 }, }; [Theory] @@ -74,7 +74,7 @@ public partial class JpegEncoderTests [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)] [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)] [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)] - public void EncodeBaseline_Interleaved(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_Interleaved(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, tolerance); [Theory] @@ -83,7 +83,7 @@ public partial class JpegEncoderTests [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)] [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)] [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)] - public void EncodeBaseline_NonInterleavedMode(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_NonInterleavedMode(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -108,7 +108,7 @@ public partial class JpegEncoderTests [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 153, 21, PixelTypes.Rgb24)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 143, 81, PixelTypes.Rgb24)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 138, 24, PixelTypes.Rgb24)] - public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality); [Theory] @@ -121,7 +121,7 @@ public partial class JpegEncoderTests [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 24, PixelTypes.Rgb24)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 46, 8, PixelTypes.Rgb24)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 51, 7, PixelTypes.Rgb24)] - public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, ImageComparer.Tolerant(0.12f)); [Theory] @@ -131,28 +131,28 @@ public partial class JpegEncoderTests [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)] public void EncodeBaseline_Grayscale(TestImageProvider provider, int quality) - where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, JpegEncodingColor.Luminance, quality); + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, JpegColorType.Luminance, quality); [Theory] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 96, 96, PixelTypes.Rgb24 | PixelTypes.Bgr24)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 48, PixelTypes.Rgb24 | PixelTypes.Bgr24)] - public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality); [Theory] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 48, PixelTypes.Rgb24 | PixelTypes.Bgr24)] - public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f)); [Theory] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)] - [WithTestPatternImages(587, 821, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)] - [WithTestPatternImages(677, 683, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)] - [WithSolidFilledImages(400, 400, nameof(Color.Red), PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)] - public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegEncodingColor colorType) + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgb24, JpegColorType.YCbCrRatio444)] + [WithTestPatternImages(587, 821, PixelTypes.Rgb24, JpegColorType.YCbCrRatio444)] + [WithTestPatternImages(677, 683, PixelTypes.Rgb24, JpegColorType.YCbCrRatio420)] + [WithSolidFilledImages(400, 400, nameof(Color.Red), PixelTypes.Rgb24, JpegColorType.YCbCrRatio420)] + public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegColorType colorType) where TPixel : unmanaged, IPixel { - ImageComparer comparer = colorType == JpegEncodingColor.YCbCrRatio444 + ImageComparer comparer = colorType == JpegColorType.YCbCrRatio444 ? ImageComparer.TolerantPercentage(0.1f) : ImageComparer.TolerantPercentage(5f); @@ -161,9 +161,9 @@ public partial class JpegEncoderTests } [Theory] - [InlineData(JpegEncodingColor.YCbCrRatio420)] - [InlineData(JpegEncodingColor.YCbCrRatio444)] - public async Task Encode_IsCancellable(JpegEncodingColor colorType) + [InlineData(JpegColorType.YCbCrRatio420)] + [InlineData(JpegColorType.YCbCrRatio444)] + public async Task Encode_IsCancellable(JpegColorType colorType) { CancellationTokenSource cts = new(); using PausedStream pausedStream = new(new MemoryStream()); @@ -201,13 +201,13 @@ public partial class JpegEncoderTests image.Mutate(x => x.Crop(132, 1606)); int[] quality = new int[] { 100, 50 }; - JpegEncodingColor[] colors = new[] { JpegEncodingColor.YCbCrRatio444, JpegEncodingColor.YCbCrRatio420 }; + JpegColorType[] colors = new[] { JpegColorType.YCbCrRatio444, JpegColorType.YCbCrRatio420 }; for (int i = 0; i < quality.Length; i++) { int q = quality[i]; for (int j = 0; j < colors.Length; j++) { - JpegEncodingColor c = colors[j]; + JpegColorType c = colors[j]; image.VerifyEncoder(provider, "jpeg", $"{q}-{c}", new JpegEncoder() { Quality = q, ColorType = c }, GetComparer(q, c)); } } @@ -216,7 +216,7 @@ public partial class JpegEncoderTests /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// - private static ImageComparer GetComparer(int quality, JpegEncodingColor? colorType) + private static ImageComparer GetComparer(int quality, JpegColorType? colorType) { float tolerance = 0.015f; // ~1.5% @@ -224,10 +224,10 @@ public partial class JpegEncoderTests { tolerance *= 4.5f; } - else if (quality < 75 || colorType == JpegEncodingColor.YCbCrRatio420) + else if (quality < 75 || colorType == JpegColorType.YCbCrRatio420) { tolerance *= 2.0f; - if (colorType == JpegEncodingColor.YCbCrRatio420) + if (colorType == JpegColorType.YCbCrRatio420) { tolerance *= 2.0f; } @@ -236,15 +236,15 @@ public partial class JpegEncoderTests return ImageComparer.Tolerant(tolerance); } - private static void TestJpegEncoderCore(TestImageProvider provider, JpegEncodingColor colorType, int quality) + private static void TestJpegEncoderCore(TestImageProvider provider, JpegColorType colorType, int quality) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, GetComparer(quality, colorType)); - private static void TestJpegEncoderCore(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + private static void TestJpegEncoderCore(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, new TolerantImageComparer(tolerance)); - private static void TestJpegEncoderCore(TestImageProvider provider, JpegEncodingColor colorType, int quality, ImageComparer comparer) + private static void TestJpegEncoderCore(TestImageProvider provider, JpegColorType colorType, int quality, ImageComparer comparer) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs index e07c42f898..19b5265a17 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -12,10 +12,10 @@ public class JpegMetadataTests [Fact] public void CloneIsDeep() { - var meta = new JpegMetadata { ColorType = JpegEncodingColor.Luminance }; + var meta = new JpegMetadata { ColorType = JpegColorType.Luminance }; var clone = (JpegMetadata)meta.DeepClone(); - clone.ColorType = JpegEncodingColor.YCbCrRatio420; + clone.ColorType = JpegColorType.YCbCrRatio420; Assert.False(meta.ColorType.Equals(clone.ColorType)); } From 0e5408123287fc955536eae56af9dedefdcbba1d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jun 2024 22:48:11 +1000 Subject: [PATCH 20/43] Fix webp bpp parsing, normalize property naming. --- src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 14 +-- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 24 ++--- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 30 +++---- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 4 +- src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs | 8 +- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 2 +- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 22 ++--- src/ImageSharp/Formats/Tga/TgaMetadata.cs | 18 ++-- .../Formats/Webp/WebpBitsPerPixel.cs | 4 +- .../Formats/Webp/WebpChunkParsingUtils.cs | 37 +++++--- src/ImageSharp/Formats/Webp/WebpColorType.cs | 25 ++++++ .../Formats/Webp/WebpDecoderCore.cs | 21 +++-- src/ImageSharp/Formats/Webp/WebpImageInfo.cs | 6 ++ src/ImageSharp/Formats/Webp/WebpMetadata.cs | 33 +++++-- .../Formats/Bmp/BmpEncoderTests.cs | 90 +++++++++---------- .../Formats/Bmp/BmpMetadataTests.cs | 4 +- .../Formats/Tga/TgaEncoderTests.cs | 32 +++---- .../Formats/Tga/TgaFileHeaderTests.cs | 8 +- .../Formats/WebP/WebpDecoderTests.cs | 4 +- 19 files changed, 229 insertions(+), 157 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/WebpColorType.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 5700bb444c..3c9e3ce795 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -11,35 +11,35 @@ public enum BmpBitsPerPixel : short /// /// 1 bit per pixel. /// - Pixel1 = 1, + Bit1 = 1, /// /// 2 bits per pixel. /// - Pixel2 = 2, + Bit2 = 2, /// /// 4 bits per pixel. /// - Pixel4 = 4, + Bit4 = 4, /// /// 8 bits per pixel. Each pixel consists of 1 byte. /// - Pixel8 = 8, + Bit8 = 8, /// /// 16 bits per pixel. Each pixel consists of 2 bytes. /// - Pixel16 = 16, + Bit16 = 16, /// /// 24 bits per pixel. Each pixel consists of 3 bytes. /// - Pixel24 = 24, + Bit24 = 24, /// /// 32 bits per pixel. Each pixel consists of 4 bytes. /// - Pixel32 = 32 + Bit32 = 32 } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 076d1adf00..24a4fa2f57 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -130,10 +130,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals int colorPaletteSize = this.bitsPerPixel switch { - BmpBitsPerPixel.Pixel8 => ColorPaletteSize8Bit, - BmpBitsPerPixel.Pixel4 => ColorPaletteSize4Bit, - BmpBitsPerPixel.Pixel2 => ColorPaletteSize2Bit, - BmpBitsPerPixel.Pixel1 => ColorPaletteSize1Bit, + BmpBitsPerPixel.Bit8 => ColorPaletteSize8Bit, + BmpBitsPerPixel.Bit4 => ColorPaletteSize4Bit, + BmpBitsPerPixel.Bit2 => ColorPaletteSize2Bit, + BmpBitsPerPixel.Bit1 => ColorPaletteSize1Bit, _ => 0 }; @@ -220,7 +220,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals clrUsed: 0, clrImportant: 0); - if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) + if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Bit32) { infoHeader.AlphaMask = Rgba32AlphaMask; infoHeader.RedMask = Rgba32RedMask; @@ -319,31 +319,31 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; switch (this.bitsPerPixel) { - case BmpBitsPerPixel.Pixel32: + case BmpBitsPerPixel.Bit32: this.Write32BitPixelData(configuration, stream, pixels); break; - case BmpBitsPerPixel.Pixel24: + case BmpBitsPerPixel.Bit24: this.Write24BitPixelData(configuration, stream, pixels); break; - case BmpBitsPerPixel.Pixel16: + case BmpBitsPerPixel.Bit16: this.Write16BitPixelData(configuration, stream, pixels); break; - case BmpBitsPerPixel.Pixel8: + case BmpBitsPerPixel.Bit8: this.Write8BitPixelData(configuration, stream, image); break; - case BmpBitsPerPixel.Pixel4: + case BmpBitsPerPixel.Bit4: this.Write4BitPixelData(configuration, stream, image); break; - case BmpBitsPerPixel.Pixel2: + case BmpBitsPerPixel.Bit2: this.Write2BitPixelData(configuration, stream, image); break; - case BmpBitsPerPixel.Pixel1: + case BmpBitsPerPixel.Bit1: this.Write1BitPixelData(configuration, stream, image); break; } diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index d44520a4f3..bad47bd0d5 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -36,7 +36,7 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata /// Gets or sets the number of bits per pixel. /// - public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; + public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Bit24; /// public static BmpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) @@ -44,23 +44,23 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel1 }, - 2 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel2 }, - <= 4 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel4 }, - <= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 }, + 1 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit1 }, + 2 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit2 }, + <= 4 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit4 }, + <= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit8 }, <= 16 => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel16, + BitsPerPixel = BmpBitsPerPixel.Bit16, InfoHeaderType = BmpInfoHeaderType.WinVersion3 }, <= 24 => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel24, + BitsPerPixel = BmpBitsPerPixel.Bit24, InfoHeaderType = BmpInfoHeaderType.WinVersion4 }, _ => new BmpMetadata { - BitsPerPixel = BmpBitsPerPixel.Pixel32, + BitsPerPixel = BmpBitsPerPixel.Bit32, InfoHeaderType = BmpInfoHeaderType.WinVersion5 } }; @@ -92,34 +92,34 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata /// /// Initializes a new instance of the class. /// - public JpegMetadata() => this.Comments = []; + public JpegMetadata() + { + } /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs index da34e62f7e..af537ddc21 100644 --- a/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs @@ -11,20 +11,20 @@ public enum TgaBitsPerPixel : byte /// /// 8 bits per pixel. Each pixel consists of 1 byte. /// - Pixel8 = 8, + Bit8 = 8, /// /// 16 bits per pixel. Each pixel consists of 2 bytes. /// - Pixel16 = 16, + Bit16 = 16, /// /// 24 bits per pixel. Each pixel consists of 3 bytes. /// - Pixel24 = 24, + Bit24 = 24, /// /// 32 bits per pixel. Each pixel consists of 4 bytes. /// - Pixel32 = 32 + Bit32 = 32 } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index b454c677bc..47f7eb0f2e 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -934,7 +934,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals return (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4); } - private bool IsTrueColor32BitPerPixel(TgaBitsPerPixel bitsPerPixel) => bitsPerPixel == TgaBitsPerPixel.Pixel32 && + private bool IsTrueColor32BitPerPixel(TgaBitsPerPixel bitsPerPixel) => bitsPerPixel == TgaBitsPerPixel.Bit32 && (this.fileHeader.ImageType == TgaImageType.TrueColor || this.fileHeader.ImageType == TgaImageType.RleTrueColor); } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 14351ee67c..8e13096568 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -59,7 +59,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals this.bitsPerPixel ??= tgaMetadata.BitsPerPixel; TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; - if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) + if (this.bitsPerPixel == TgaBitsPerPixel.Bit8) { imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; } @@ -71,13 +71,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals imageDescriptor |= 0x20; } - if (this.bitsPerPixel is TgaBitsPerPixel.Pixel32) + if (this.bitsPerPixel is TgaBitsPerPixel.Bit32) { // Indicate, that 8 bit are used for the alpha channel. imageDescriptor |= 0x8; } - if (this.bitsPerPixel is TgaBitsPerPixel.Pixel16) + if (this.bitsPerPixel is TgaBitsPerPixel.Bit16) { // Indicate, that 1 bit is used for the alpha channel. imageDescriptor |= 0x1; @@ -130,19 +130,19 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals Buffer2D pixels = image.PixelBuffer; switch (this.bitsPerPixel) { - case TgaBitsPerPixel.Pixel8: + case TgaBitsPerPixel.Bit8: this.Write8Bit(configuration, stream, pixels); break; - case TgaBitsPerPixel.Pixel16: + case TgaBitsPerPixel.Bit16: this.Write16Bit(configuration, stream, pixels); break; - case TgaBitsPerPixel.Pixel24: + case TgaBitsPerPixel.Bit24: this.Write24Bit(configuration, stream, pixels); break; - case TgaBitsPerPixel.Pixel32: + case TgaBitsPerPixel.Bit32: this.Write32Bit(configuration, stream, pixels); break; } @@ -208,12 +208,12 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals { switch (this.bitsPerPixel) { - case TgaBitsPerPixel.Pixel8: + case TgaBitsPerPixel.Bit8: L8 l8 = L8.FromRgba32(color); stream.WriteByte(l8.PackedValue); break; - case TgaBitsPerPixel.Pixel16: + case TgaBitsPerPixel.Bit16: Bgra5551 bgra5551 = Bgra5551.FromRgba32(color); Span buffer = stackalloc byte[2]; BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue); @@ -222,13 +222,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals break; - case TgaBitsPerPixel.Pixel24: + case TgaBitsPerPixel.Bit24: stream.WriteByte(color.B); stream.WriteByte(color.G); stream.WriteByte(color.R); break; - case TgaBitsPerPixel.Pixel32: + case TgaBitsPerPixel.Bit32: stream.WriteByte(color.B); stream.WriteByte(color.G); stream.WriteByte(color.R); diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index fa8e8b6f4f..58b5119523 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -27,7 +27,7 @@ public class TgaMetadata : IFormatMetadata /// /// Gets or sets the number of bits per pixel. /// - public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; + public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Bit24; /// /// Gets or sets the number of alpha bits per pixel. @@ -41,10 +41,10 @@ public class TgaMetadata : IFormatMetadata int bpp = metadata.PixelTypeInfo.BitsPerPixel; return bpp switch { - <= 8 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel8 }, - <= 16 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel16 }, - <= 24 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel24 }, - _ => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Pixel32 } + <= 8 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit8 }, + <= 16 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit16 }, + <= 24 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit24 }, + _ => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit32 } }; } @@ -57,22 +57,22 @@ public class TgaMetadata : IFormatMetadata PixelAlphaRepresentation alpha; switch (this.BitsPerPixel) { - case TgaBitsPerPixel.Pixel8: + case TgaBitsPerPixel.Bit8: info = PixelComponentInfo.Create(1, bpp, 8); color = PixelColorType.Luminance; alpha = PixelAlphaRepresentation.None; break; - case TgaBitsPerPixel.Pixel16: + case TgaBitsPerPixel.Bit16: info = PixelComponentInfo.Create(1, bpp, 5, 5, 5, 1); color = PixelColorType.BGR | PixelColorType.Alpha; alpha = PixelAlphaRepresentation.Unassociated; break; - case TgaBitsPerPixel.Pixel24: + case TgaBitsPerPixel.Bit24: info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); color = PixelColorType.RGB; alpha = PixelAlphaRepresentation.None; break; - case TgaBitsPerPixel.Pixel32 or _: + case TgaBitsPerPixel.Bit32 or _: info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); color = PixelColorType.RGB | PixelColorType.Alpha; alpha = PixelAlphaRepresentation.Unassociated; diff --git a/src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs b/src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs index 529c4bafb9..03717d852a 100644 --- a/src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs @@ -11,10 +11,10 @@ public enum WebpBitsPerPixel : short /// /// 24 bits per pixel. Each pixel consists of 3 bytes. /// - Pixel24 = 24, + Bit24 = 24, /// /// 32 bits per pixel. Each pixel consists of 4 bytes (an alpha channel is present). /// - Pixel32 = 32 + Bit32 = 32 } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 07f09d45ea..4ccaf65031 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -17,6 +17,10 @@ internal static class WebpChunkParsingUtils /// /// Reads the header of a lossy webp image. /// + /// The memory allocator. + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. /// Information about this webp image. public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) { @@ -114,13 +118,15 @@ internal static class WebpChunkParsingUtils Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; - return new WebpImageInfo + return new() { Width = width, Height = height, XScale = xScale, YScale = yScale, - BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24, + + // Vp8 header can be parsed during the processing of the Vp8X header. + BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, IsLossless = false, Features = features, Vp8Profile = (sbyte)version, @@ -132,7 +138,10 @@ internal static class WebpChunkParsingUtils /// /// Reads the header of a lossless webp image. /// - /// Information about this image. + /// The memory allocator. + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) { // VP8 data size. @@ -156,8 +165,8 @@ internal static class WebpChunkParsingUtils } // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. - // TODO: this flag value is not used yet - bool alphaIsUsed = bitReader.ReadBit(); + // Alpha may have already been set by the VP8X chunk. + features.Alpha |= bitReader.ReadBit(); // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. // Any other value should be treated as an error. @@ -167,11 +176,11 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new WebpImageInfo + return new() { Width = width, Height = height, - BitsPerPixel = WebpBitsPerPixel.Pixel32, + BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, IsLossless = true, Features = features, Vp8LBitReader = bitReader @@ -187,6 +196,9 @@ internal static class WebpChunkParsingUtils /// - An optional 'ALPH' chunk with alpha channel data. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. /// + /// The buffered read stream. + /// The scratch buffer to use while reading. + /// The webp features to parse. /// Information about this webp image. public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span buffer, WebpFeatures features) { @@ -217,11 +229,8 @@ internal static class WebpChunkParsingUtils features.Animation = (imageFeatures & (1 << 1)) != 0; // 3 reserved bytes should follow which are supposed to be zero. + // No other decoder actually checks this though. stream.Read(buffer, 0, 3); - if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0) - { - WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); - } // 3 bytes for the width. uint width = ReadUInt24LittleEndian(stream, buffer) + 1; @@ -230,14 +239,14 @@ internal static class WebpChunkParsingUtils uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new() + return new() { Width = width, Height = height, Features = features - }; - return info; + // Additional properties are set during the parsing of the VP8 or VP8L headers. + }; } /// diff --git a/src/ImageSharp/Formats/Webp/WebpColorType.cs b/src/ImageSharp/Formats/Webp/WebpColorType.cs new file mode 100644 index 0000000000..64d9143b1f --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpColorType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Provides enumeration of the various webp color types. +/// +public enum WebpColorType +{ + /// + /// Yuv (luminance, blue chroma, red chroma) as defined in the ITU-R Rec. BT.709 specification. + /// + Yuv, + + /// + /// Rgb color space. + /// + Rgb, + + /// + /// Rgba color space. + /// + Rgba +} diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 21f0f4946b..c29742da5b 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -186,36 +186,43 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + WebpImageInfo? info = null; WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: + info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); webpMetadata.FileFormat = WebpFileFormatType.Lossy; - return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpMetadata.ColorType = WebpColorType.Yuv; + return info; case WebpChunkType.Vp8L: + info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); webpMetadata.FileFormat = WebpFileFormatType.Lossless; - return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + return info; case WebpChunkType.Vp8X: - WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); + info = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); while (stream.Position < stream.Length) { chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Vp8) { + info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; } else if (chunkType == WebpChunkType.Vp8L) { + info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { - return webpInfos; + return info; } } else @@ -226,7 +233,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable } } - return webpInfos; + return info; default: WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); return diff --git a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs index 5f7301b262..3428ce199a 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs @@ -18,8 +18,14 @@ internal class WebpImageInfo : IDisposable /// public uint Height { get; set; } + /// + /// Gets or sets the horizontal scale. + /// public sbyte XScale { get; set; } + /// + /// Gets or sets the vertical scale. + /// public sbyte YScale { get; set; } /// diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 536ea09294..d8cd29d142 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Provides Webp specific metadata information for the image. /// -public class WebpMetadata : IDeepCloneable +public class WebpMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -21,15 +21,27 @@ public class WebpMetadata : IDeepCloneable /// The metadata to create an instance from. private WebpMetadata(WebpMetadata other) { + this.BitsPerPixel = other.BitsPerPixel; + this.ColorType = other.ColorType; this.FileFormat = other.FileFormat; this.RepeatCount = other.RepeatCount; this.BackgroundColor = other.BackgroundColor; } + /// + /// Gets or sets the number of bits per pixel. + /// + public WebpBitsPerPixel BitsPerPixel { get; set; } = WebpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color type. + /// + public WebpColorType ColorType { get; set; } = WebpColorType.Rgba; + /// /// Gets or sets the webp file format used. Either lossless or lossy. /// - public WebpFileFormatType? FileFormat { get; set; } + public WebpFileFormatType FileFormat { get; set; } = WebpFileFormatType.Lossless; /// /// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely. @@ -44,9 +56,6 @@ public class WebpMetadata : IDeepCloneable /// public Color BackgroundColor { get; set; } - /// - public IDeepCloneable DeepClone() => new WebpMetadata(this); - internal static WebpMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) => new() { @@ -54,4 +63,18 @@ public class WebpMetadata : IDeepCloneable BackgroundColor = metadata.BackgroundColor, RepeatCount = metadata.RepeatCount }; + + /// + public static WebpMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => throw new NotImplementedException(); + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => throw new NotImplementedException(); + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public WebpMetadata DeepClone() => new(this); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 42cbd90f3b..a0fbc210f8 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -22,8 +22,8 @@ public class BmpEncoderTests public static readonly TheoryData BitsPerPixel = new() { - BmpBitsPerPixel.Pixel24, - BmpBitsPerPixel.Pixel32 + BmpBitsPerPixel.Bit24, + BmpBitsPerPixel.Bit32 }; public static readonly TheoryData RatioFiles = @@ -37,13 +37,13 @@ public class BmpEncoderTests public static readonly TheoryData BmpBitsPerPixelFiles = new() { - { Bit1, BmpBitsPerPixel.Pixel1 }, - { Bit2, BmpBitsPerPixel.Pixel2 }, - { Bit4, BmpBitsPerPixel.Pixel4 }, - { Bit8, BmpBitsPerPixel.Pixel8 }, - { Rgb16, BmpBitsPerPixel.Pixel16 }, - { Car, BmpBitsPerPixel.Pixel24 }, - { Bit32Rgb, BmpBitsPerPixel.Pixel32 } + { Bit1, BmpBitsPerPixel.Bit1 }, + { Bit2, BmpBitsPerPixel.Bit2 }, + { Bit4, BmpBitsPerPixel.Bit4 }, + { Bit8, BmpBitsPerPixel.Bit8 }, + { Rgb16, BmpBitsPerPixel.Bit16 }, + { Car, BmpBitsPerPixel.Bit24 }, + { Bit32Rgb, BmpBitsPerPixel.Bit32 } }; [Fact] @@ -97,61 +97,61 @@ public class BmpEncoderTests where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel); [Theory] - [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] + [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] public void Encode_32Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) // If supportTransparency is false, a v3 bitmap header will be written. where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] - [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] - [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] + [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] + [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Bit32)] public void Encode_32Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] - [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] // WinBmpv3 is a 24 bits per pixel image. - [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] + [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Bit24)] // WinBmpv3 is a 24 bits per pixel image. + [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Bit24)] public void Encode_24Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] - [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] - [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] + [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Bit24)] + [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Bit24)] public void Encode_24Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] - [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] - [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] + [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Bit16)] + [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Bit16)] public void Encode_16Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] - [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] - [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] + [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Bit16)] + [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Bit16)] public void Encode_16Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] - [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] - [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] + [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Bit8)] + [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Bit8)] public void Encode_8Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] - [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] - [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] + [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Bit8)] + [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Bit8)] public void Encode_8Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] - [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] + [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Bit8)] public void Encode_8BitGray_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore( @@ -160,7 +160,7 @@ public class BmpEncoderTests supportTransparency: false); [Theory] - [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] + [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Bit4)] public void Encode_4Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) @@ -176,7 +176,7 @@ public class BmpEncoderTests } [Theory] - [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] + [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Bit4)] public void Encode_4Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) @@ -192,7 +192,7 @@ public class BmpEncoderTests } [Theory] - [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel2)] + [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Bit2)] public void Encode_2Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) @@ -214,7 +214,7 @@ public class BmpEncoderTests } [Theory] - [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel2)] + [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Bit2)] public void Encode_2Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) @@ -236,21 +236,21 @@ public class BmpEncoderTests } [Theory] - [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] + [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Bit1)] public void Encode_1Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] - [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] + [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Bit1)] public void Encode_1Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] - [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] + [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Bit8)] public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore( @@ -271,7 +271,7 @@ public class BmpEncoderTests using Image image = provider.GetImage(); BmpEncoder encoder = new() { - BitsPerPixel = BmpBitsPerPixel.Pixel8, + BitsPerPixel = BmpBitsPerPixel.Bit8, Quantizer = new WuQuantizer() }; @@ -303,7 +303,7 @@ public class BmpEncoderTests using Image image = provider.GetImage(); BmpEncoder encoder = new() { - BitsPerPixel = BmpBitsPerPixel.Pixel8, + BitsPerPixel = BmpBitsPerPixel.Bit8, Quantizer = new OctreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); @@ -322,8 +322,8 @@ public class BmpEncoderTests } [Theory] - [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] - [WithFile(Bit32Rgba, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] + [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Bit32)] + [WithFile(Bit32Rgba, PixelTypes.Rgba32, BmpBitsPerPixel.Bit32)] public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); @@ -364,8 +364,8 @@ public class BmpEncoderTests } [Theory] - [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] - [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] + [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Bit32)] + [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Bit32)] public void Encode_WorksWithDiscontiguousBuffers(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { @@ -374,7 +374,7 @@ public class BmpEncoderTests } [Theory] - [WithFile(BlackWhitePalletDataMatrix, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel1)] + [WithFile(BlackWhitePalletDataMatrix, PixelTypes.Rgb24, BmpBitsPerPixel.Bit1)] public void Encode_Issue2467(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { @@ -409,7 +409,7 @@ public class BmpEncoderTests using Image image = provider.GetImage(); // There is no alpha in bmp with less then 32 bits per pixels, so the reference image will be made opaque. - if (bitsPerPixel != BmpBitsPerPixel.Pixel32) + if (bitsPerPixel != BmpBitsPerPixel.Bit32) { image.Mutate(c => c.MakeOpaque()); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index 1d84356713..f7a398ec14 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -15,10 +15,10 @@ public class BmpMetadataTests public void CloneIsDeep() { BmpMetadata meta = new() - { BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.Os2Version2 }; + { BitsPerPixel = BmpBitsPerPixel.Bit24, InfoHeaderType = BmpInfoHeaderType.Os2Version2 }; BmpMetadata clone = (BmpMetadata)meta.DeepClone(); - clone.BitsPerPixel = BmpBitsPerPixel.Pixel32; + clone.BitsPerPixel = BmpBitsPerPixel.Bit32; clone.InfoHeaderType = BmpInfoHeaderType.WinVersion2; Assert.False(meta.BitsPerPixel.Equals(clone.BitsPerPixel)); diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index a3fa082c62..c9f93446f4 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -15,17 +15,17 @@ public class TgaEncoderTests public static readonly TheoryData BitsPerPixel = new() { - TgaBitsPerPixel.Pixel24, - TgaBitsPerPixel.Pixel32 + TgaBitsPerPixel.Bit24, + TgaBitsPerPixel.Bit32 }; public static readonly TheoryData TgaBitsPerPixelFiles = new() { - { Gray8BitBottomLeft, TgaBitsPerPixel.Pixel8 }, - { Bit16BottomLeft, TgaBitsPerPixel.Pixel16 }, - { Bit24BottomLeft, TgaBitsPerPixel.Pixel24 }, - { Bit32BottomLeft, TgaBitsPerPixel.Pixel32 }, + { Gray8BitBottomLeft, TgaBitsPerPixel.Bit8 }, + { Bit16BottomLeft, TgaBitsPerPixel.Bit16 }, + { Bit24BottomLeft, TgaBitsPerPixel.Bit24 }, + { Bit32BottomLeft, TgaBitsPerPixel.Bit32 }, }; [Theory] @@ -73,46 +73,46 @@ public class TgaEncoderTests [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + public void TgaEncoder_Bit8_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit8) // Using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + public void TgaEncoder_Bit16_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit16) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None, useExactComparer: false); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + public void TgaEncoder_Bit24_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit24) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + public void TgaEncoder_Bit32_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit32) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.None); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel8) + public void TgaEncoder_Bit8_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit8) // Using tolerant comparer here. The results from magick differ slightly. Maybe a different ToGrey method is used. The image looks otherwise ok. where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false, compareTolerance: 0.03f); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel16) + public void TgaEncoder_Bit16_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit16) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength, useExactComparer: false); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel24) + public void TgaEncoder_Bit24_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit24) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); [Theory] [WithFile(Bit32BottomLeft, PixelTypes.Rgba32)] - public void TgaEncoder_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) + public void TgaEncoder_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Bit32) where TPixel : unmanaged, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); [Theory] @@ -152,8 +152,8 @@ public class TgaEncoderTests } [Theory] - [WithFile(Bit32BottomLeft, PixelTypes.Rgba32, TgaBitsPerPixel.Pixel32)] - [WithFile(Bit24BottomLeft, PixelTypes.Rgba32, TgaBitsPerPixel.Pixel24)] + [WithFile(Bit32BottomLeft, PixelTypes.Rgba32, TgaBitsPerPixel.Bit32)] + [WithFile(Bit24BottomLeft, PixelTypes.Rgba32, TgaBitsPerPixel.Bit24)] public void TgaEncoder_WorksWithDiscontiguousBuffers(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs index bf24ba3500..e5954719af 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs @@ -33,10 +33,10 @@ public class TgaFileHeaderTests } [Theory] - [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 195, 0, 32, 8 }, 250, 195, TgaBitsPerPixel.Pixel32)] - [InlineData(new byte[] { 26, 1, 9, 0, 0, 0, 1, 16, 0, 0, 0, 0, 128, 0, 128, 0, 8, 0 }, 128, 128, TgaBitsPerPixel.Pixel8)] - [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 220, 0, 16, 0 }, 220, 220, TgaBitsPerPixel.Pixel16)] - [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 0, 124, 0, 24, 32 }, 124, 124, TgaBitsPerPixel.Pixel24)] + [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 195, 0, 32, 8 }, 250, 195, TgaBitsPerPixel.Bit32)] + [InlineData(new byte[] { 26, 1, 9, 0, 0, 0, 1, 16, 0, 0, 0, 0, 128, 0, 128, 0, 8, 0 }, 128, 128, TgaBitsPerPixel.Bit8)] + [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 220, 0, 16, 0 }, 220, 220, TgaBitsPerPixel.Bit16)] + [InlineData(new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 0, 124, 0, 24, 32 }, 124, 124, TgaBitsPerPixel.Bit24)] public void Identify_WithValidData_Works(byte[] data, int width, int height, TgaBitsPerPixel bitsPerPixel) { using MemoryStream stream = new(data); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 0dda304b64..99f4dee168 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -30,8 +30,8 @@ public class WebpDecoderTests [Theory] [InlineData(Lossless.GreenTransform1, 1000, 307, 32)] - [InlineData(Lossless.BikeThreeTransforms, 250, 195, 32)] - [InlineData(Lossless.NoTransform2, 128, 128, 32)] + [InlineData(Lossless.BikeThreeTransforms, 250, 195, 24)] + [InlineData(Lossless.NoTransform2, 128, 128, 24)] [InlineData(Lossy.Alpha1, 1000, 307, 32)] [InlineData(Lossy.Alpha2, 1000, 307, 32)] [InlineData(Lossy.BikeWithExif, 250, 195, 24)] From c01bdedbae48bfbbc8a4808ee9dde55fa864ad74 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jun 2024 23:24:04 +1000 Subject: [PATCH 21/43] Update WebpMetadata.cs --- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 90 +++++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index d8cd29d142..a7100dd26e 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Webp; /// /// Provides Webp specific metadata information for the image. /// -public class WebpMetadata : IFormatFrameMetadata +public class WebpMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -65,12 +67,90 @@ public class WebpMetadata : IFormatFrameMetadata }; /// - public static WebpMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) - => throw new NotImplementedException(); + public static WebpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + WebpBitsPerPixel bitsPerPixel; + WebpColorType color; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB | PixelColorType.Alpha; + switch (colorType) + { + case PixelColorType.RGB: + case PixelColorType.BGR: + color = WebpColorType.Rgb; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + case PixelColorType.YCbCr: + color = WebpColorType.Yuv; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + default: + if (colorType.HasFlag(PixelColorType.Alpha)) + { + color = WebpColorType.Rgba; + bitsPerPixel = WebpBitsPerPixel.Bit32; + break; + } + + color = WebpColorType.Rgb; + bitsPerPixel = WebpBitsPerPixel.Bit24; + break; + } + + return new() + { + BitsPerPixel = bitsPerPixel, + ColorType = color, + FileFormat = WebpFileFormatType.Lossless, + BackgroundColor = metadata.BackgroundColor, + RepeatCount = metadata.RepeatCount + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp; + PixelColorType colorType; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + PixelComponentInfo info; + switch (this.ColorType) + { + case WebpColorType.Yuv: + bpp = 24; + colorType = PixelColorType.YCbCr; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + case WebpColorType.Rgb: + bpp = 24; + colorType = PixelColorType.RGB; + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + break; + case WebpColorType.Rgba: + default: + bpp = 32; + colorType = PixelColorType.RGB | PixelColorType.Alpha; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ColorType = colorType, + ComponentInfo = info, + }; + } /// - public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => throw new NotImplementedException(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTableMode = FrameColorTableMode.Global, + RepeatCount = this.RepeatCount, + BackgroundColor = this.BackgroundColor + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); From baaf29cec50180e82505bc2a7f9d53b7d3c57113 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 12 Jun 2024 09:40:43 +1000 Subject: [PATCH 22/43] Fix default webp file format --- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index a7100dd26e..a42435fd4a 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -43,7 +43,7 @@ public class WebpMetadata : IFormatMetadata /// /// Gets or sets the webp file format used. Either lossless or lossy. /// - public WebpFileFormatType FileFormat { get; set; } = WebpFileFormatType.Lossless; + public WebpFileFormatType FileFormat { get; set; } = WebpFileFormatType.Lossy; /// /// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely. From 9a0e289d25f54ef89dfea1e85ca8d673a745a70c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 13 Jun 2024 11:01:35 +1000 Subject: [PATCH 23/43] Complete conversion APIs --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 1 - src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 10 +- .../Formats/Bmp/MetadataExtensions.cs | 20 -- src/ImageSharp/Formats/EncodingType.cs | 20 ++ .../Formats/FormatConnectingMetadata.cs | 7 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 49 +-- src/ImageSharp/Formats/Gif/GifMetadata.cs | 28 -- .../Formats/Gif/MetadataExtensions.cs | 97 ------ .../Formats/IFormatFrameMetadata.cs | 2 +- src/ImageSharp/Formats/IFormatMetadata.cs | 2 +- src/ImageSharp/Formats/ImageDecoder.cs | 21 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 3 +- .../Formats/Jpeg/MetadataExtensions.cs | 21 -- .../Formats/Pbm/IPbmEncoderOptions.cs | 25 -- .../Formats/Pbm/MetadataExtensions.cs | 20 -- src/ImageSharp/Formats/Pbm/PbmEncoder.cs | 2 - src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs | 1 - src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 2 +- .../Formats/Png/MetadataExtensions.cs | 84 ------ src/ImageSharp/Formats/Png/PngDecoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 56 +--- .../Formats/Png/PngFrameMetadata.cs | 24 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 35 +-- .../Formats/Qoi/MetadataExtensions.cs | 20 -- src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 2 +- .../Formats/Tga/MetadataExtensions.cs | 20 -- .../Formats/Tiff/MetadataExtensions.cs | 27 -- .../Formats/Tiff/TiffEncoderCore.cs | 1 - .../Formats/Webp/Chunks/WebpFrameData.cs | 20 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 6 +- .../Formats/Webp/MetadataExtensions.cs | 75 ----- .../Formats/Webp/WebpAnimationDecoder.cs | 4 +- .../Formats/Webp/WebpBlendMethod.cs | 22 -- .../Formats/Webp/WebpCommonUtils.cs | 50 ---- .../Formats/Webp/WebpDisposalMethod.cs | 20 -- .../Formats/Webp/WebpEncoderCore.cs | 23 +- .../Formats/Webp/WebpFileFormatType.cs | 4 +- .../Formats/Webp/WebpFrameMetadata.cs | 37 ++- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 17 +- .../{ => _Generated}/ImageExtensions.Save.cs | 2 - .../{ => _Generated}/ImageExtensions.Save.tt | 19 +- .../_Generated/ImageMetadataExtensions.cs | 283 ++++++++++++++++++ .../_Generated/ImageMetadataExtensions.tt | 77 +++++ .../Formats/_Generated/_Formats.ttinclude | 24 ++ src/ImageSharp/ImageSharp.csproj | 13 +- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 59 ++-- src/ImageSharp/Metadata/ImageMetadata.cs | 52 ++-- src/ImageSharp/PixelFormats/PixelTypeInfo.cs | 4 +- .../Formats/Bmp/BmpMetadataTests.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 26 +- .../Formats/Pbm/PbmMetadataTests.cs | 1 - .../Formats/Png/PngEncoderTests.cs | 8 +- .../Formats/WebP/WebpEncoderTests.cs | 28 +- 54 files changed, 606 insertions(+), 874 deletions(-) delete mode 100644 src/ImageSharp/Formats/Bmp/MetadataExtensions.cs create mode 100644 src/ImageSharp/Formats/EncodingType.cs delete mode 100644 src/ImageSharp/Formats/Gif/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs delete mode 100644 src/ImageSharp/Formats/Pbm/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Png/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Qoi/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Tga/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Tiff/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Webp/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpBlendMethod.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs rename src/ImageSharp/Formats/{ => _Generated}/ImageExtensions.Save.cs (99%) rename src/ImageSharp/Formats/{ => _Generated}/ImageExtensions.Save.tt (95%) create mode 100644 src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs create mode 100644 src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt create mode 100644 src/ImageSharp/Formats/_Generated/_Formats.ttinclude diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 24a4fa2f57..298bb8dbca 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index bad47bd0d5..c19ab44cf7 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// Provides Bmp specific metadata information for the image. /// -public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata +public class BmpMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -66,10 +66,6 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata - public static BmpMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) - => new(); - /// public PixelTypeInfo GetPixelTypeInfo() { @@ -140,10 +136,6 @@ public class BmpMetadata : IFormatMetadata, IFormatFrameMetadata - public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => new(); - /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Bmp/MetadataExtensions.cs b/src/ImageSharp/Formats/Bmp/MetadataExtensions.cs deleted file mode 100644 index 5297d0c989..0000000000 --- a/src/ImageSharp/Formats/Bmp/MetadataExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the bmp format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static BmpMetadata GetBmpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(BmpFormat.Instance); -} diff --git a/src/ImageSharp/Formats/EncodingType.cs b/src/ImageSharp/Formats/EncodingType.cs new file mode 100644 index 0000000000..f4567ca43d --- /dev/null +++ b/src/ImageSharp/Formats/EncodingType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Provides a way to specify the type of encoding to be used. +/// +public enum EncodingType +{ + /// + /// Lossless encoding, which compresses data without any loss of information. + /// + Lossless, + + /// + /// Lossy encoding, which compresses data by discarding some of it. + /// + Lossy +} diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index 2acbe96990..07579c09e8 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -11,7 +11,12 @@ namespace SixLabors.ImageSharp.Formats; public class FormatConnectingMetadata { /// - /// Gets the quality. + /// Gets the encoding type. + /// + public EncodingType EncodingType { get; init; } + + /// + /// Gets the quality to use when is . /// /// /// The value is usually between 1 and 100. Defaults to 100. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 5fd20d6ae2..90177ccc1b 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -5,8 +5,6 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -85,7 +83,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - GifMetadata gifMetadata = GetGifMetadata(image); + GifMetadata gifMetadata = image.Metadata.CloneGifMetadata(); this.colorTableMode ??= gifMetadata.ColorTableMode; bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global; @@ -178,56 +176,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals quantized?.Dispose(); } - private static GifMetadata GetGifMetadata(Image image) - where TPixel : unmanaged, IPixel - { - if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) - { - return gif.DeepClone(); - } - - if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) - { - AnimatedImageMetadata ani = png.ToAnimatedImageMetadata(); - return GifMetadata.FromAnimatedMetadata(ani); - } - - if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) - { - AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata(); - return GifMetadata.FromAnimatedMetadata(ani); - } - - // Return explicit new instance so we do not mutate the original metadata. - return new(); - } - private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex) where TPixel : unmanaged, IPixel { - GifFrameMetadata? metadata = null; - if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) - { - metadata = gif.DeepClone(); - } - else if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) - { - AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata(); - metadata = GifFrameMetadata.FromAnimatedMetadata(ani); - } - else if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp)) - { - AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata(); - metadata = GifFrameMetadata.FromAnimatedMetadata(ani); - } - - if (metadata?.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1) + GifFrameMetadata metadata = frame.Metadata.CloneGifMetadata(); + if (metadata.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1) { metadata.HasTransparency = true; metadata.TransparencyIndex = ClampIndex(transparencyIndex); } - return metadata ?? new(); + return metadata; } private void EncodeAdditionalFrames( diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 90d3312c85..565038b55a 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -69,34 +69,6 @@ public class GifMetadata : IFormatMetadata /// public IList Comments { get; set; } = []; - internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) - { - int index = 0; - Color background = metadata.BackgroundColor; - if (metadata.ColorTable.HasValue) - { - ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; - for (int i = 0; i < colorTable.Length; i++) - { - if (background != colorTable[i]) - { - continue; - } - - index = i; - break; - } - } - - return new() - { - GlobalColorTable = metadata.ColorTable, - ColorTableMode = metadata.ColorTableMode, - RepeatCount = metadata.RepeatCount, - BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255), - }; - } - /// public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs deleted file mode 100644 index 0f2d281f16..0000000000 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the gif format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static GifMetadata GetGifMetadata(this ImageMetadata source) - => source.GetFormatMetadata(GifFormat.Instance); - - /// - /// Gets the gif format specific metadata for the image. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified image, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the gif metadata exists; otherwise, . - /// - public static bool TryGetGifMetadata(this ImageMetadata source, [NotNullWhen(true)] out GifMetadata? metadata) - => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); - - /// - /// Gets the gif format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) - => source.GetFormatMetadata(GifFormat.Instance); - - /// - /// Gets the gif format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified frame, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the gif frame metadata exists; otherwise, . - /// - public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) - => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); - - internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source) - { - Color background = Color.Transparent; - if (source.GlobalColorTable != null) - { - background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex]; - } - - return new() - { - ColorTable = source.GlobalColorTable, - ColorTableMode = source.ColorTableMode, - RepeatCount = source.RepeatCount, - BackgroundColor = background, - }; - } - - internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source) - { - // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or - // has a local palette with 256 colors and is not transparent we should use 'Source'. - bool blendSource = source.DisposalMode == FrameDisposalMode.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); - - // If the color table is global and frame has no transparency. Consider it 'Source' also. - blendSource |= source.ColorTableMode == FrameColorTableMode.Global && !source.HasTransparency; - - return new() - { - ColorTable = source.LocalColorTable, - ColorTableMode = source.ColorTableMode, - Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), - DisposalMode = source.DisposalMode, - BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, - }; - } -} diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs index 4c5321073d..4eef93ad34 100644 --- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs +++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -20,7 +20,7 @@ public interface IFormatFrameMetadata : IDeepCloneable /// /// The metadata type implementing this interface. public interface IFormatFrameMetadata : IFormatFrameMetadata, IDeepCloneable - where TSelf : class, IFormatFrameMetadata, new() + where TSelf : class, IFormatFrameMetadata { /// /// Creates a new instance of the class from the given . diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs index e0c32e8b26..8d695306e4 100644 --- a/src/ImageSharp/Formats/IFormatMetadata.cs +++ b/src/ImageSharp/Formats/IFormatMetadata.cs @@ -28,7 +28,7 @@ public interface IFormatMetadata : IDeepCloneable /// /// The metadata type implementing this interface. public interface IFormatMetadata : IFormatMetadata, IDeepCloneable - where TSelf : class, IFormatMetadata, new() + where TSelf : class, IFormatMetadata { /// /// Creates a new instance of the class from the given . diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index ebb45d7013..4e79f79efc 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -231,7 +232,7 @@ public abstract class ImageDecoder : IImageDecoder throw new NotSupportedException("Cannot read from the stream."); } - Task PeformActionAndResetPosition(Stream s, long position, CancellationToken ct) + Task PerformActionAndResetPosition(Stream s, long position, CancellationToken ct) { try { @@ -263,15 +264,15 @@ public abstract class ImageDecoder : IImageDecoder // code below to copy the stream to an in-memory buffer before invoking the action. if (stream is MemoryStream ms) { - return PeformActionAndResetPosition(ms, ms.Position, cancellationToken); + return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } if (stream is ChunkedMemoryStream cms) { - return PeformActionAndResetPosition(cms, cms.Position, cancellationToken); + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); } - return CopyToMemoryStreamAndActionAsync(options, stream, PeformActionAndResetPosition, cancellationToken); + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } private static async Task CopyToMemoryStreamAndActionAsync( @@ -282,7 +283,7 @@ public abstract class ImageDecoder : IImageDecoder { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); @@ -293,6 +294,11 @@ public abstract class ImageDecoder : IImageDecoder if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) { image.Metadata.DecodedImageFormat = format; + + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.DecodedImageFormat = format; + } } } @@ -301,6 +307,11 @@ public abstract class ImageDecoder : IImageDecoder if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) { info.Metadata.DecodedImageFormat = format; + + foreach (ImageFrameMetadata frame in info.FrameMetadataCollection) + { + frame.DecodedImageFormat = format; + } } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 8b123fa35d..f2f34ec496 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -112,7 +112,7 @@ public class JpegMetadata : IFormatMetadata public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { JpegColorType color; - PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.YCbCr; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; switch (colorType) { case PixelColorType.Luminance: @@ -194,6 +194,7 @@ public class JpegMetadata : IFormatMetadata public FormatConnectingMetadata ToFormatConnectingMetadata() => new() { + EncodingType = EncodingType.Lossy, PixelTypeInfo = this.GetPixelTypeInfo(), Quality = this.Quality, }; diff --git a/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs b/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs deleted file mode 100644 index 7330e74b79..0000000000 --- a/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Text; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the jpeg format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static JpegMetadata GetJpegMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(JpegFormat.Instance); -} diff --git a/src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs b/src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs deleted file mode 100644 index 7039ef2620..0000000000 --- a/src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Pbm; - -/// -/// Configuration options for use during PBM encoding. -/// -internal interface IPbmEncoderOptions -{ - /// - /// Gets the encoding of the pixels. - /// - PbmEncoding? Encoding { get; } - - /// - /// Gets the Color type of the resulting image. - /// - PbmColorType? ColorType { get; } - - /// - /// Gets the Data Type of the pixel components. - /// - PbmComponentType? ComponentType { get; } -} diff --git a/src/ImageSharp/Formats/Pbm/MetadataExtensions.cs b/src/ImageSharp/Formats/Pbm/MetadataExtensions.cs deleted file mode 100644 index 6d44e91a50..0000000000 --- a/src/ImageSharp/Formats/Pbm/MetadataExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Pbm; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the pbm format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static PbmMetadata GetPbmMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PbmFormat.Instance); -} diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs index 8258c91655..f7a9d79366 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Pbm; /// diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index b6e31a3c28..5ae37d4e69 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Text; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm; diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index 49a36de41e..9b23aecac2 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -46,7 +46,7 @@ public class PbmMetadata : IFormatMetadata public static PbmMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { PbmColorType color; - PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.Luminance; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; switch (colorType) { diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs deleted file mode 100644 index 6da1fb5fdb..0000000000 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the png format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); - - /// - /// Gets the png format specific metadata for the image. - /// - /// The metadata this method extends. - /// The metadata. - /// - /// if the png metadata exists; otherwise, . - /// - public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(true)] out PngMetadata? metadata) - => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); - - /// - /// Gets the png format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); - - /// - /// Gets the png format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The metadata. - /// - /// if the png frame metadata exists; otherwise, . - /// - public static bool TryGetPngMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) - => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); - - internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source) - => new() - { - ColorTable = source.ColorTable, - ColorTableMode = FrameColorTableMode.Global, - RepeatCount = (ushort)Numerics.Clamp(source.RepeatCount, 0, ushort.MaxValue), - }; - - internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source) - { - double delay = source.FrameDelay.ToDouble(); - if (double.IsNaN(delay)) - { - delay = 0; - } - - return new() - { - ColorTableMode = FrameColorTableMode.Global, - Duration = TimeSpan.FromMilliseconds(delay * 1000), - DisposalMode = GetMode(source.DisposalMode), - BlendMode = source.BlendMode, - }; - } - - private static FrameDisposalMode GetMode(FrameDisposalMode method) => method switch - { - FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, - FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, - _ => FrameDisposalMode.DoNotDispose, - }; -} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index e62a14b5e0..cfea0e6020 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -101,5 +101,5 @@ public sealed class PngDecoder : SpecializedImageDecoder } /// - protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new PngDecoderOptions() { GeneralOptions = options }; + protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index d1528d08b4..a5ab739882 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -9,10 +9,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; -using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -160,7 +158,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.height = image.Height; ImageMetadata metadata = image.Metadata; - PngMetadata pngMetadata = GetPngMetadata(image); + PngMetadata pngMetadata = metadata.ClonePngMetadata(); this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); stream.Write(PngConstants.HeaderBytes); @@ -211,7 +209,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { // Write the first animated frame. currentFrame = image.Frames[currentFrameIndex]; - PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); + PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata(); FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); uint sequenceNumber = 1; @@ -241,7 +239,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable currentFrame = image.Frames[currentFrameIndex]; ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; - frameMetadata = GetPngFrameMetadata(currentFrame); + frameMetadata = currentFrame.Metadata.GetPngMetadata(); bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; (bool difference, Rectangle bounds) = @@ -288,54 +286,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.currentScanline?.Dispose(); } - private static PngMetadata GetPngMetadata(Image image) - where TPixel : unmanaged, IPixel - { - if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) - { - return png.DeepClone(); - } - - if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) - { - AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata(); - return PngMetadata.FromAnimatedMetadata(ani); - } - - if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) - { - AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata(); - return PngMetadata.FromAnimatedMetadata(ani); - } - - // Return explicit new instance so we do not mutate the original metadata. - return new(); - } - - private static PngFrameMetadata GetPngFrameMetadata(ImageFrame frame) - where TPixel : unmanaged, IPixel - { - if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) - { - return png.DeepClone(); - } - - if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) - { - AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata(); - return PngFrameMetadata.FromAnimatedMetadata(ani); - } - - if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp)) - { - AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata(); - return PngFrameMetadata.FromAnimatedMetadata(ani); - } - - // Return explicit new instance so we do not mutate the original metadata. - return new(); - } - /// /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index de141909ad..c142a1c8e0 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -57,22 +57,6 @@ public class PngFrameMetadata : IFormatFrameMetadata this.BlendMode = frameControl.BlendMode; } - internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) - => new() - { - FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), - DisposalMode = GetMode(metadata.DisposalMode), - BlendMode = metadata.BlendMode, - }; - - private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch - { - FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, - FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, - _ => FrameDisposalMode.DoNotDispose, - }; - /// public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => new() @@ -105,4 +89,12 @@ public class PngFrameMetadata : IFormatFrameMetadata /// public PngFrameMetadata DeepClone() => new(this); + + private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch + { + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + _ => FrameDisposalMode.DoNotDispose, + }; } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index d72dc51841..beb4ca5ae8 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -90,38 +90,6 @@ public class PngMetadata : IFormatMetadata /// public bool AnimateRootFrame { get; set; } = true; - internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) - { - // Should the conversion be from a format that uses a 24bit palette entries (gif) - // we need to clone and adjust the color table to allow for transparency. - Color[]? colorTable = metadata.ColorTable?.ToArray(); - if (colorTable != null) - { - for (int i = 0; i < colorTable.Length; i++) - { - ref Color c = ref colorTable[i]; - if (c != metadata.BackgroundColor) - { - continue; - } - - // Png treats background as fully empty - c = Color.Transparent; - break; - } - } - - return new() - { - ColorType = colorTable != null ? PngColorType.Palette : PngColorType.RgbWithAlpha, - BitDepth = colorTable != null - ? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8) - : PngBitDepth.Bit8, - ColorTable = colorTable, - RepeatCount = metadata.RepeatCount, - }; - } - /// public static PngMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { @@ -145,8 +113,7 @@ public class PngMetadata : IFormatMetadata } PngColorType color; - PixelColorType colorType = - metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB | PixelColorType.Alpha; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; switch (colorType) { diff --git a/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs b/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs deleted file mode 100644 index 1e0fa88997..0000000000 --- a/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Qoi; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the qoi format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance); -} diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index c8bcd97480..e2062014d7 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -40,7 +40,7 @@ public class QoiMetadata : IFormatMetadata /// public static QoiMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { - PixelColorType color = metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB; + PixelColorType color = metadata.PixelTypeInfo.ColorType; if (color.HasFlag(PixelColorType.Alpha)) { diff --git a/src/ImageSharp/Formats/Tga/MetadataExtensions.cs b/src/ImageSharp/Formats/Tga/MetadataExtensions.cs deleted file mode 100644 index 8d5e357641..0000000000 --- a/src/ImageSharp/Formats/Tga/MetadataExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Tga; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the tga format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static TgaMetadata GetTgaMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TgaFormat.Instance); -} diff --git a/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs deleted file mode 100644 index b06f5dd470..0000000000 --- a/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the tiff format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static TiffMetadata GetTiffMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); - - /// - /// Gets the tiff format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); -} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 546ff7947d..c8eeb1e31f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -395,7 +395,6 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals case TiffBitsPerPixel.Bit42: case TiffBitsPerPixel.Bit48: // Encoding not yet supported bits per pixel will default to 24 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); break; case TiffBitsPerPixel.Bit64: diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs index c8ff579a89..66662569c3 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -10,7 +10,7 @@ internal readonly struct WebpFrameData /// public const uint HeaderSize = 16; - public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod) + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod) { this.DataSize = dataSize; this.X = x; @@ -30,12 +30,12 @@ internal readonly struct WebpFrameData width, height, duration, - (flags & 2) == 0 ? WebpBlendMethod.Over : WebpBlendMethod.Source, - (flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose) + (flags & 2) == 0 ? FrameBlendMode.Over : FrameBlendMode.Source, + (flags & 1) == 1 ? FrameDisposalMode.RestoreToBackground : FrameDisposalMode.DoNotDispose) { } - public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod) + public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod) : this(0, x, y, width, height, duration, blendingMethod, disposalMethod) { } @@ -74,12 +74,12 @@ internal readonly struct WebpFrameData /// /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// - public WebpBlendMethod BlendingMethod { get; } + public FrameBlendMode BlendingMethod { get; } /// /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// - public WebpDisposalMethod DisposalMethod { get; } + public FrameDisposalMode DisposalMethod { get; } public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height); @@ -91,13 +91,13 @@ internal readonly struct WebpFrameData { byte flags = 0; - if (this.BlendingMethod is WebpBlendMethod.Source) + if (this.BlendingMethod is FrameBlendMode.Source) { // Set blending flag. flags |= 2; } - if (this.DisposalMethod is WebpDisposalMethod.RestoreToBackground) + if (this.DisposalMethod is FrameDisposalMode.RestoreToBackground) { // Set disposal flag. flags |= 1; @@ -124,7 +124,7 @@ internal readonly struct WebpFrameData { Span buffer = stackalloc byte[4]; - WebpFrameData data = new( + return new( dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, @@ -132,7 +132,5 @@ internal readonly struct WebpFrameData height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), flags: stream.ReadByte()); - - return data; } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index f658e40f6f..58d007265b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -259,7 +259,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 40d91ecf1a..7e1ef93cfe 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -332,7 +332,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } @@ -376,7 +376,7 @@ internal class Vp8Encoder : IDisposable where TPixel : unmanaged, IPixel { ImageFrame frame = image.Frames.RootFrame; - this.Encode(stream, frame, image.Bounds, WebpCommonUtils.GetWebpFrameMetadata(frame), false, image); + this.Encode(stream, frame, image.Bounds, frame.Metadata.GetWebpMetadata(), false, image); } /// @@ -462,7 +462,7 @@ internal class Vp8Encoder : IDisposable // Extract and encode alpha channel data, if present. int alphaDataSize = 0; bool alphaCompressionSucceeded = false; - Span alphaData = Span.Empty; + Span alphaData = []; IMemoryOwner encodedAlphaData = null; try { diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs deleted file mode 100644 index 731d3f1ff2..0000000000 --- a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the webp format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance); - - /// - /// Gets the webp format specific metadata for the image. - /// - /// The metadata this method extends. - /// The metadata. - /// - /// if the webp metadata exists; otherwise, . - /// - public static bool TryGetWebpMetadata(this ImageMetadata source, [NotNullWhen(true)] out WebpMetadata? metadata) - => source.TryGetFormatMetadata(WebpFormat.Instance, out metadata); - - /// - /// Gets the webp format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance); - - /// - /// Gets the webp format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The metadata. - /// - /// if the webp frame metadata exists; otherwise, . - /// - public static bool TryGetWebpFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out WebpFrameMetadata? metadata) - => source.TryGetFormatMetadata(WebpFormat.Instance, out metadata); - - internal static AnimatedImageMetadata ToAnimatedImageMetadata(this WebpMetadata source) - => new() - { - ColorTableMode = FrameColorTableMode.Global, - RepeatCount = source.RepeatCount, - BackgroundColor = source.BackgroundColor - }; - - internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this WebpFrameMetadata source) - => new() - { - ColorTableMode = FrameColorTableMode.Global, - Duration = TimeSpan.FromMilliseconds(source.FrameDelay), - DisposalMode = GetMode(source.DisposalMethod), - BlendMode = source.BlendMethod == WebpBlendMethod.Over ? FrameBlendMode.Over : FrameBlendMode.Source, - }; - - private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch - { - WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - WebpDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose, - _ => FrameDisposalMode.DoNotDispose, - }; -} diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 70372fe985..72405e480e 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -195,14 +195,14 @@ internal class WebpAnimationDecoder : IDisposable Rectangle regionRectangle = frameData.Bounds; - if (frameData.DisposalMethod is WebpDisposalMethod.RestoreToBackground) + if (frameData.DisposalMethod is FrameDisposalMode.RestoreToBackground) { this.RestoreToBackground(imageFrame, backgroundColor); } using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over; + bool blend = previousFrame != null && frameData.BlendingMethod == FrameBlendMode.Over; DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); previousFrame = currentFrame ?? image.Frames.RootFrame; diff --git a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs deleted file mode 100644 index f16f7650c7..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. -/// -public enum WebpBlendMethod -{ - /// - /// Do not blend. After disposing of the previous frame, - /// render the current frame on the canvas by overwriting the rectangle covered by the current frame. - /// - Source = 0, - - /// - /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. - /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. - /// - Over = 1, -} diff --git a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs index 49482260bb..a1e9821c09 100644 --- a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs @@ -4,8 +4,6 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -15,54 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// internal static class WebpCommonUtils { - public static WebpMetadata GetWebpMetadata(Image image) - where TPixel : unmanaged, IPixel - { - if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) - { - return (WebpMetadata)webp.DeepClone(); - } - - if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) - { - AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata(); - return WebpMetadata.FromAnimatedMetadata(ani); - } - - if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) - { - AnimatedImageMetadata ani = png.ToAnimatedImageMetadata(); - return WebpMetadata.FromAnimatedMetadata(ani); - } - - // Return explicit new instance so we do not mutate the original metadata. - return new(); - } - - public static WebpFrameMetadata GetWebpFrameMetadata(ImageFrame frame) - where TPixel : unmanaged, IPixel - { - if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp)) - { - return (WebpFrameMetadata)webp.DeepClone(); - } - - if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) - { - AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata(); - return WebpFrameMetadata.FromAnimatedMetadata(ani); - } - - if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) - { - AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata(); - return WebpFrameMetadata.FromAnimatedMetadata(ani); - } - - // Return explicit new instance so we do not mutate the original metadata. - return new(); - } - /// /// Checks if the pixel row is not opaque. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs deleted file mode 100644 index 47cc83951d..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. -/// -public enum WebpDisposalMethod -{ - /// - /// Do not dispose. Leave the canvas as is. - /// - DoNotDispose = 0, - - /// - /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk. - /// - RestoreToBackground = 1 -} diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d29759f9a1..21ce444b11 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -124,7 +125,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless; } @@ -150,12 +151,12 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; - WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); + WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation); if (hasAnimation) { - WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. @@ -163,12 +164,12 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals for (int i = 1; i < image.Frames.Count; i++) { - ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); - bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over; + frameMetadata = currentFrame.Metadata.GetWebpMetadata(); + bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -227,8 +228,8 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; - WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); - WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); + FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); @@ -238,12 +239,12 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals for (int i = 1; i < image.Frames.Count; i++) { - ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); - bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over; + frameMetadata = currentFrame.Metadata.GetWebpMetadata(); + bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( diff --git a/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs b/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs index 1ed9bbb431..6f606cdf46 100644 --- a/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs +++ b/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs @@ -9,12 +9,12 @@ namespace SixLabors.ImageSharp.Formats.Webp; public enum WebpFileFormatType { /// - /// The lossless webp format. + /// The lossless Webp format, which compresses data without any loss of information. /// Lossless, /// - /// The lossy webp format. + /// The lossy Webp format, which compresses data by discarding some of it. /// Lossy, } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index cd1b5d5905..45e182d223 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Provides webp specific metadata information for the image frame. /// -public class WebpFrameMetadata : IDeepCloneable +public class WebpFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -29,12 +29,12 @@ public class WebpFrameMetadata : IDeepCloneable /// /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// - public WebpBlendMethod BlendMethod { get; set; } + public FrameBlendMode BlendMethod { get; set; } /// /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// - public WebpDisposalMethod DisposalMethod { get; set; } + public FrameDisposalMode DisposalMethod { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, @@ -43,13 +43,34 @@ public class WebpFrameMetadata : IDeepCloneable public uint FrameDelay { get; set; } /// - public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); - - internal static WebpFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) + public static WebpFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => new() { FrameDelay = (uint)metadata.Duration.TotalMilliseconds, - BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? WebpBlendMethod.Source : WebpBlendMethod.Over, - DisposalMethod = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose + BlendMethod = metadata.BlendMode, + DisposalMethod = GetMode(metadata.DisposalMode) + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + ColorTableMode = FrameColorTableMode.Global, + Duration = TimeSpan.FromMilliseconds(this.FrameDelay), + DisposalMode = this.DisposalMethod, + BlendMode = this.BlendMethod, }; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public WebpFrameMetadata DeepClone() => new(this); + + private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch + { + FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground, + FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose, + _ => FrameDisposalMode.DoNotDispose, + }; } diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index a42435fd4a..33ebbbf6dc 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -54,24 +54,16 @@ public class WebpMetadata : IFormatMetadata /// Gets or sets the default background color of the canvas when animating. /// This color may be used to fill the unused space on the canvas around the frames, /// as well as the transparent pixels of the first frame. - /// The background color is also used when the Disposal method is . + /// The background color is also used when the Disposal method is . /// public Color BackgroundColor { get; set; } - internal static WebpMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) - => new() - { - FileFormat = WebpFileFormatType.Lossless, - BackgroundColor = metadata.BackgroundColor, - RepeatCount = metadata.RepeatCount - }; - /// public static WebpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) { WebpBitsPerPixel bitsPerPixel; WebpColorType color; - PixelColorType colorType = metadata.PixelTypeInfo.ColorType ?? PixelColorType.RGB | PixelColorType.Alpha; + PixelColorType colorType = metadata.PixelTypeInfo.ColorType; switch (colorType) { case PixelColorType.RGB: @@ -100,9 +92,9 @@ public class WebpMetadata : IFormatMetadata { BitsPerPixel = bitsPerPixel, ColorType = color, - FileFormat = WebpFileFormatType.Lossless, BackgroundColor = metadata.BackgroundColor, - RepeatCount = metadata.RepeatCount + RepeatCount = metadata.RepeatCount, + FileFormat = metadata.EncodingType == EncodingType.Lossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy }; } @@ -146,6 +138,7 @@ public class WebpMetadata : IFormatMetadata public FormatConnectingMetadata ToFormatConnectingMetadata() => new() { + EncodingType = this.FileFormat == WebpFileFormatType.Lossless ? EncodingType.Lossless : EncodingType.Lossy, PixelTypeInfo = this.GetPixelTypeInfo(), ColorTableMode = FrameColorTableMode.Global, RepeatCount = this.RepeatCount, diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs similarity index 99% rename from src/ImageSharp/Formats/ImageExtensions.Save.cs rename to src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index 7e5989d6fc..5d7f84acfb 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -2,8 +2,6 @@ // Licensed under the Six Labors Split License. // -using SixLabors.ImageSharp.Advanced; - using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt similarity index 95% rename from src/ImageSharp/Formats/ImageExtensions.Save.tt rename to src/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt index d4f1ed233b..144dd83625 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.tt @@ -1,25 +1,8 @@ -<#@ template language="C#" #> +<#@include file="_Formats.ttinclude" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - // -using SixLabors.ImageSharp.Advanced; - <# - var formats = new []{ - "Bmp", - "Gif", - "Jpeg", - "Pbm", - "Png", - "Qoi", - "Tga", - "Tiff", - "Webp", - }; - foreach (string fmt in formats) { #> diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs new file mode 100644 index 0000000000..826f5905b1 --- /dev/null +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -0,0 +1,283 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the and types. +/// +public static class ImageMetadataExtensions +{ + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static BmpMetadata GetBmpMetadata(this ImageMetadata source) => source.GetFormatMetadata(BmpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static JpegMetadata GetJpegMetadata(this ImageMetadata source) => source.GetFormatMetadata(JpegFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static JpegMetadata CloneJpegMetadata(this ImageMetadata source) => source.CloneFormatMetadata(JpegFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static PbmMetadata GetPbmMetadata(this ImageMetadata source) => source.GetFormatMetadata(PbmFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static PbmMetadata ClonePbmMetadata(this ImageMetadata source) => source.CloneFormatMetadata(PbmFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static PngMetadata ClonePngMetadata(this ImageMetadata source) => source.CloneFormatMetadata(PngFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static QoiMetadata GetQoiMetadata(this ImageMetadata source) => source.GetFormatMetadata(QoiFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static QoiMetadata CloneQoiMetadata(this ImageMetadata source) => source.CloneFormatMetadata(QoiFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static TgaMetadata GetTgaMetadata(this ImageMetadata source) => source.GetFormatMetadata(TgaFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static TgaMetadata CloneTgaMetadata(this ImageMetadata source) => source.CloneFormatMetadata(TgaFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static TiffMetadata GetTiffMetadata(this ImageMetadata source) => source.GetFormatMetadata(TiffFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static TiffMetadata CloneTiffMetadata(this ImageMetadata source) => source.CloneFormatMetadata(TiffFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static WebpMetadata GetWebpMetadata(this ImageMetadata source) => source.GetFormatMetadata(WebpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static GifFrameMetadata CloneGifMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static PngFrameMetadata ClonePngMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(PngFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(TiffFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static TiffFrameMetadata CloneTiffMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(TiffFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(WebpFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static WebpFrameMetadata CloneWebpMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); +} diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt new file mode 100644 index 0000000000..e4db85ed59 --- /dev/null +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt @@ -0,0 +1,77 @@ +<#@include file="_Formats.ttinclude" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +// +using SixLabors.ImageSharp.Metadata; +<# + foreach (string fmt in formats) + { +#> +using SixLabors.ImageSharp.Formats.<#= fmt #>; +<# + + } +#> + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the and types. +/// +public static class ImageMetadataExtensions +{ +<# + foreach (string fmt in formats) + { +#> + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static <#= fmt #>Metadata Get<#= fmt #>Metadata(this ImageMetadata source) => source.GetFormatMetadata(<#= fmt #>Format.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static <#= fmt #>Metadata Clone<#= fmt #>Metadata(this ImageMetadata source) => source.CloneFormatMetadata(<#= fmt #>Format.Instance); + +<# + } +#> +<# + foreach (string fmt in frameFormats) + { +#> + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static <#= fmt #>FrameMetadata Get<#= fmt #>Metadata(this ImageFrameMetadata source) => source.GetFormatMetadata(<#= fmt #>Format.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static <#= fmt #>FrameMetadata Clone<#= fmt #>Metadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(<#= fmt #>Format.Instance); +<# + } +#> +} diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude new file mode 100644 index 0000000000..24ac66a70b --- /dev/null +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -0,0 +1,24 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +<#+ + private static readonly string[] formats = new []{ + "Bmp", + "Gif", + "Jpeg", + "Pbm", + "Png", + "Qoi", + "Tga", + "Tiff", + "Webp", + }; + + private static readonly string[] frameFormats = new []{ + "Gif", + "Png", + "Tiff", + "Webp", + }; +#> diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 6096bd33e3..d3c4034717 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -52,6 +52,11 @@ + + True + True + ImageMetadataExtensions.tt + True True @@ -142,7 +147,7 @@ True PorterDuffFunctions.Generated.tt - + True True ImageExtensions.Save.tt @@ -150,6 +155,10 @@ + + ImageMetadataExtensions.cs + TextTemplatingFileGenerator + TextTemplatingFileGenerator Block8x8F.Generated.cs @@ -222,7 +231,7 @@ DefaultPixelBlenders.Generated.cs TextTemplatingFileGenerator - + TextTemplatingFileGenerator ImageExtensions.Save.cs diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 1c0330d5d0..afda71879f 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata; ///
public sealed class ImageFrameMetadata : IDeepCloneable { - private readonly Dictionary formatMetadata = new(); + private readonly Dictionary formatMetadata = []; /// /// Initializes a new instance of the class. @@ -35,9 +35,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable { DebugGuard.NotNull(other, nameof(other)); - foreach (KeyValuePair meta in other.formatMetadata) + foreach (KeyValuePair meta in other.formatMetadata) { - this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); + this.formatMetadata.Add(meta.Key, (IFormatFrameMetadata)meta.Value.DeepClone()); } this.ExifProfile = other.ExifProfile?.DeepClone(); @@ -45,6 +45,10 @@ public sealed class ImageFrameMetadata : IDeepCloneable this.IptcProfile = other.IptcProfile?.DeepClone(); this.XmpProfile = other.XmpProfile?.DeepClone(); this.CicpProfile = other.CicpProfile?.DeepClone(); + + // NOTE: This clone is actually shallow but we share the same format + // instances for all images in the configuration. + this.DecodedImageFormat = other.DecodedImageFormat; } /// @@ -72,12 +76,19 @@ public sealed class ImageFrameMetadata : IDeepCloneable /// public CicpProfile? CicpProfile { get; set; } + /// + /// Gets the original format, if any, the image was decode from. + /// + public IImageFormat? DecodedImageFormat { get; internal set; } + /// public ImageFrameMetadata DeepClone() => new(this); /// - /// Gets the metadata value associated with the specified key. This method will always return a result creating - /// a new instance and binding it to the frame metadata if none is found. + /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. ///
/// The type of format metadata. /// The type of format frame metadata. @@ -87,43 +98,37 @@ public sealed class ImageFrameMetadata : IDeepCloneable /// public TFormatFrameMetadata GetFormatMetadata(IImageFormat key) where TFormatMetadata : class - where TFormatFrameMetadata : class, IDeepCloneable + where TFormatFrameMetadata : class, IFormatFrameMetadata { - if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta)) + if (this.formatMetadata.TryGetValue(key, out IFormatFrameMetadata? meta)) { return (TFormatFrameMetadata)meta; } + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) + { + return TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + } + TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); this.formatMetadata[key] = newMeta; return newMeta; } /// - /// Gets the metadata value associated with the specified key. + /// Creates a new instance the metadata value associated with the specified key. + /// The instance is created from a clone generated via . /// - /// The type of format metadata. + /// The type of metadata. /// The type of format frame metadata. /// The key of the value to get. - /// - /// When this method returns, contains the metadata associated with the specified key, - /// if the key is found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// /// - /// if the frame metadata exists for the specified key; otherwise, . + /// The . /// - public bool TryGetFormatMetadata(IImageFormat key, out TFormatFrameMetadata? metadata) + public TFormatFrameMetadata CloneFormatMetadata(IImageFormat key) where TFormatMetadata : class - where TFormatFrameMetadata : class, IDeepCloneable - { - if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta)) - { - metadata = (TFormatFrameMetadata)meta; - return true; - } - - metadata = default; - return false; - } + where TFormatFrameMetadata : class, IFormatFrameMetadata + => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index ceeea42cfc..b521f8eeed 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -33,7 +33,7 @@ public sealed class ImageMetadata : IDeepCloneable ///
public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch; - private readonly Dictionary formatMetadata = []; + private readonly Dictionary formatMetadata = []; private double horizontalResolution; private double verticalResolution; @@ -60,9 +60,9 @@ public sealed class ImageMetadata : IDeepCloneable this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - foreach (KeyValuePair meta in other.formatMetadata) + foreach (KeyValuePair meta in other.formatMetadata) { - this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); + this.formatMetadata.Add(meta.Key, (IFormatMetadata)meta.Value.DeepClone()); } this.ExifProfile = other.ExifProfile?.DeepClone(); @@ -170,7 +170,10 @@ public sealed class ImageMetadata : IDeepCloneable public IImageFormat? DecodedImageFormat { get; internal set; } /// - /// Gets the metadata value associated with the specified key. + /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. ///
/// The type of metadata. /// The key of the value to get. @@ -178,47 +181,44 @@ public sealed class ImageMetadata : IDeepCloneable /// The . /// public TFormatMetadata GetFormatMetadata(IImageFormat key) - where TFormatMetadata : class, IDeepCloneable + where TFormatMetadata : class, IFormatMetadata { - if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta)) + // Check for existing metadata. + if (this.formatMetadata.TryGetValue(key, out IFormatMetadata? meta)) { return (TFormatMetadata)meta; } + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) + { + return TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + } + + // Fall back to a default instance. TFormatMetadata newMeta = key.CreateDefaultFormatMetadata(); this.formatMetadata[key] = newMeta; return newMeta; } /// - /// Gets the metadata value associated with the specified key. + /// Creates a new instance the metadata value associated with the specified key. + /// The instance is created from a clone generated via . /// - /// The type of format metadata. + /// The type of metadata. /// The key of the value to get. - /// - /// When this method returns, contains the metadata associated with the specified key, - /// if the key is found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// /// - /// if the frame metadata exists for the specified key; otherwise, . + /// The . /// - public bool TryGetFormatMetadata(IImageFormat key, out TFormatMetadata? metadata) - where TFormatMetadata : class, IDeepCloneable - { - if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta)) - { - metadata = (TFormatMetadata)meta; - return true; - } - - metadata = default; - return false; - } + public TFormatMetadata CloneFormatMetadata(IImageFormat key) + where TFormatMetadata : class, IFormatMetadata, new() + => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); /// public ImageMetadata DeepClone() => new(this); + /// TODO: This should be called on save. /// /// Synchronizes the profiles with the current metadata. /// diff --git a/src/ImageSharp/PixelFormats/PixelTypeInfo.cs b/src/ImageSharp/PixelFormats/PixelTypeInfo.cs index 7cd1284f48..7865b9900e 100644 --- a/src/ImageSharp/PixelFormats/PixelTypeInfo.cs +++ b/src/ImageSharp/PixelFormats/PixelTypeInfo.cs @@ -31,13 +31,13 @@ public readonly struct PixelTypeInfo(int bitsPerPixel) /// /// Gets the pixel color type. /// - public PixelColorType? ColorType { get; init; } + public PixelColorType ColorType { get; init; } /// /// Gets the pixel alpha transparency behavior. /// means unknown, unspecified. /// - public PixelAlphaRepresentation? AlphaRepresentation { get; init; } + public PixelAlphaRepresentation AlphaRepresentation { get; init; } /// /// Creates a new instance. diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index f7a398ec14..64564ae1d8 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -16,7 +16,7 @@ public class BmpMetadataTests { BmpMetadata meta = new() { BitsPerPixel = BmpBitsPerPixel.Bit24, InfoHeaderType = BmpInfoHeaderType.Os2Version2 }; - BmpMetadata clone = (BmpMetadata)meta.DeepClone(); + BmpMetadata clone = meta.DeepClone(); clone.BitsPerPixel = BmpBitsPerPixel.Bit32; clone.InfoHeaderType = BmpInfoHeaderType.WinVersion2; diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index aeb3bab7bb..77ac51e8a1 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -242,33 +242,11 @@ public class GifEncoderTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - - int count = 0; - foreach (ImageFrame frame in image.Frames) - { - if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _)) - { - count++; - } - } - provider.Utility.SaveTestOutputFile(image, extension: "gif"); using FileStream fs = File.OpenRead(provider.Utility.GetTestOutputFileName("gif")); using Image image2 = Image.Load(fs); - Assert.Equal(image.Frames.Count, image2.Frames.Count); - - count = 0; - foreach (ImageFrame frame in image2.Frames) - { - if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _)) - { - count++; - } - } - - Assert.Equal(image2.Frames.Count, count); } [Theory] @@ -358,10 +336,10 @@ public class GifEncoderTests switch (webpF.DisposalMethod) { - case WebpDisposalMethod.RestoreToBackground: + case FrameDisposalMode.RestoreToBackground: Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMode); break; - case WebpDisposalMethod.DoNotDispose: + case FrameDisposalMode.DoNotDispose: default: Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMode); break; diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs index a69d9d9ba7..243a62d592 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Pbm; using static SixLabors.ImageSharp.Tests.TestImages.Pbm; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index f40e537a15..009327c17f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -526,10 +526,10 @@ public partial class PngEncoderTests switch (webpF.BlendMethod) { - case WebpBlendMethod.Source: + case FrameBlendMode.Source: Assert.Equal(FrameBlendMode.Source, pngF.BlendMode); break; - case WebpBlendMethod.Over: + case FrameBlendMode.Over: default: Assert.Equal(FrameBlendMode.Over, pngF.BlendMode); break; @@ -537,10 +537,10 @@ public partial class PngEncoderTests switch (webpF.DisposalMethod) { - case WebpDisposalMethod.RestoreToBackground: + case FrameDisposalMode.RestoreToBackground: Assert.Equal(FrameDisposalMode.RestoreToBackground, pngF.DisposalMode); break; - case WebpDisposalMethod.DoNotDispose: + case FrameDisposalMode.DoNotDispose: default: Assert.Equal(FrameDisposalMode.DoNotDispose, pngF.DisposalMode); break; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 4d72363075..aff1b35946 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -98,13 +98,13 @@ public class WebpEncoderTests switch (gifF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: - Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod); break; case FrameDisposalMode.RestoreToPrevious: case FrameDisposalMode.Unspecified: case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod); break; } } @@ -147,22 +147,22 @@ public class WebpEncoderTests switch (pngF.BlendMode) { case FrameBlendMode.Source: - Assert.Equal(WebpBlendMethod.Source, webpF.BlendMethod); + Assert.Equal(FrameBlendMode.Source, webpF.BlendMethod); break; case FrameBlendMode.Over: default: - Assert.Equal(WebpBlendMethod.Over, webpF.BlendMethod); + Assert.Equal(FrameBlendMode.Over, webpF.BlendMethod); break; } switch (pngF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: - Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod); break; case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod); break; } } @@ -220,7 +220,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossless", "_q", quality); + string testOutputDetails = $"lossless_q{quality}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } @@ -250,7 +250,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossless", "_m", method, "_q", quality); + string testOutputDetails = $"lossless_m{method}_q{quality}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } @@ -290,7 +290,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("nearlossless", "_q", nearLosslessQuality); + string testOutputDetails = $"nearlossless_q{nearLosslessQuality}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(nearLosslessQuality)); } @@ -314,7 +314,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossless", "_m", method); + string testOutputDetails = $"lossless_m{method}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } @@ -344,7 +344,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossy", "_q", quality); + string testOutputDetails = $"lossy_q{quality}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); } @@ -364,7 +364,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossy", "_f", filterStrength); + string testOutputDetails = $"lossy_f{filterStrength}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); } @@ -384,7 +384,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossy", "_sns", snsStrength); + string testOutputDetails = $"lossy_sns{snsStrength}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); } @@ -414,7 +414,7 @@ public class WebpEncoderTests }; using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossy", "_m", method, "_q", quality); + string testOutputDetails = $"lossy_m{method}_q{quality}"; image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); } From a06511f6d2a7fce35033a17e8e367a9d3c6fd838 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 13 Jun 2024 17:13:28 +1000 Subject: [PATCH 24/43] Initialize pixel type info from decoder. --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 1 - src/ImageSharp/Formats/ImageDecoder.cs | 1 + .../Formats/Jpeg/JpegDecoderCore.cs | 2 +- src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 10 +- src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 18 ++-- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 25 +---- src/ImageSharp/Formats/Png/PngMetadata.cs | 12 +-- src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 2 +- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 1 - .../Formats/Tiff/TiffDecoderCore.cs | 2 +- .../Formats/Webp/WebpDecoderCore.cs | 1 - src/ImageSharp/ImageInfo.cs | 25 ++--- src/ImageSharp/Metadata/ImageMetadata.cs | 16 +++- .../Formats/Bmp/BmpDecoderTests.cs | 21 +++-- .../Formats/Bmp/BmpEncoderTests.cs | 18 ++-- .../Formats/Jpg/JpegDecoderTests.cs | 2 +- .../Formats/Png/PngDecoderTests.cs | 6 +- .../Formats/Qoi/QoiEncoderTests.cs | 4 +- .../Formats/Tga/TgaEncoderTests.cs | 18 ++-- .../Formats/Tiff/TiffDecoderBaseTester.cs | 2 +- .../Formats/Tiff/TiffEncoderBaseTester.cs | 3 +- .../Formats/WebP/WebpDecoderTests.cs | 2 +- .../Formats/WebP/WebpEncoderTests.cs | 9 +- .../Formats/WebP/YuvConversionTests.cs | 2 +- .../Image/ImageTests.ImageLoadTestBase.cs | 3 +- tests/ImageSharp.Tests/ImageInfoTests.cs | 25 +++-- tests/ImageSharp.Tests/TestFormat.cs | 14 +-- .../ReferenceCodecs/MagickReferenceDecoder.cs | 43 +++++++-- .../ReferenceCodecUtilities.cs | 94 +++++++++++++++++++ .../SystemDrawingReferenceDecoder.cs | 26 +++-- .../SystemDrawingReferenceEncoder.cs | 1 + .../TestUtilities/TestEnvironment.Formats.cs | 4 +- .../Tests/MagickReferenceCodecTests.cs | 15 ++- .../Tests/ReferenceDecoderBenchmarks.cs | 8 +- .../Tests/SystemDrawingReferenceCodecTests.cs | 2 +- .../Tests/TestImageProviderTests.cs | 26 +++-- 37 files changed, 306 insertions(+), 160 deletions(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index bed489752c..9391d52dfe 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -208,7 +208,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadImageHeaders(stream, out _, out _); - return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); + return new ImageInfo(new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); } /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e110acc30d..8bd3e3ba2c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -249,7 +249,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } return new ImageInfo( - new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), this.metadata, framesMetadata); diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 4e79f79efc..42e265dae8 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -307,6 +307,7 @@ public abstract class ImageDecoder : IImageDecoder if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) { info.Metadata.DecodedImageFormat = format; + info.PixelType = info.Metadata.GetDecodedPixelTypeInfo(); foreach (ImageFrameMetadata frame in info.FrameMetadataCollection) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 6028aab300..759cc700f5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -226,7 +226,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals this.InitDerivedMetadataProperties(); Size pixelSize = this.Frame.PixelSize; - return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), new(pixelSize.Width, pixelSize.Height), this.Metadata); + return new ImageInfo(new(pixelSize.Width, pixelSize.Height), this.Metadata); } /// diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 3fe339865b..590fb2fb07 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -69,7 +69,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals { this.ProcessHeader(stream); - var image = new Image(this.configuration, this.pixelSize.Width, this.pixelSize.Height, this.metadata); + Image image = new(this.configuration, this.pixelSize.Width, this.pixelSize.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); @@ -86,10 +86,9 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); - - // BlackAndWhite pixels are encoded into a byte. - int bitsPerPixel = this.componentType == PbmComponentType.Short ? 16 : 8; - return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new(this.pixelSize.Width, this.pixelSize.Height), this.metadata); + return new ImageInfo( + new(this.pixelSize.Width, this.pixelSize.Height), + this.metadata); } /// @@ -97,6 +96,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals /// /// The input stream. /// An EOF marker has been read before the image has been decoded. + [MemberNotNull(nameof(metadata))] private void ProcessHeader(BufferedReadStream stream) { Span buffer = stackalloc byte[2]; diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index 9b23aecac2..fec4beda7c 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -97,20 +97,20 @@ public class PbmMetadata : IFormatMetadata colorType = PixelColorType.Binary; info = PixelComponentInfo.Create(1, bpp, 1); break; - case PbmColorType.Grayscale: - bpp = 8; - colorType = PixelColorType.Luminance; - info = PixelComponentInfo.Create(1, bpp, 8); - break; case PbmColorType.Rgb: - bpp = 24; + bpp = this.ComponentType == PbmComponentType.Short ? 48 : 24; colorType = PixelColorType.RGB; - info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + info = this.ComponentType == PbmComponentType.Short + ? PixelComponentInfo.Create(3, bpp, 16, 16, 16) + : PixelComponentInfo.Create(3, bpp, 8, 8, 8); break; + case PbmColorType.Grayscale: default: - bpp = 8; + bpp = this.ComponentType == PbmComponentType.Short ? 16 : 8; colorType = PixelColorType.Luminance; - info = PixelComponentInfo.Create(1, bpp, 8); + info = this.ComponentType == PbmComponentType.Short + ? PixelComponentInfo.Create(1, bpp, bpp) + : PixelComponentInfo.Create(1, bpp, bpp); break; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a4d13177d8..49fab870ba 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -515,7 +515,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(this.header.Width, this.header.Height), metadata); } finally { @@ -680,29 +680,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } - /// - /// Calculates the correct number of bits per pixel for the given color type. - /// - /// The - private int CalculateBitsPerPixel() - { - switch (this.pngColorType) - { - case PngColorType.Grayscale: - case PngColorType.Palette: - return this.header.BitDepth; - case PngColorType.GrayscaleWithAlpha: - return this.header.BitDepth * 2; - case PngColorType.Rgb: - return this.header.BitDepth * 3; - case PngColorType.RgbWithAlpha: - return this.header.BitDepth * 4; - default: - PngThrowHelper.ThrowNotSupportedColor(); - return -1; - } - } - /// /// Calculates the correct number of bytes per pixel for the given color type. /// diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index beb4ca5ae8..a7b3672ef5 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -176,9 +176,9 @@ public class PngMetadata : IFormatMetadata break; case PngColorType.Grayscale: - bpp = 8; + bpp = (int)this.BitDepth; colorType = PixelColorType.Luminance; - info = PixelComponentInfo.Create(1, bpp, 8); + info = PixelComponentInfo.Create(1, bpp, bpp); break; case PngColorType.GrayscaleWithAlpha: @@ -200,15 +200,15 @@ public class PngMetadata : IFormatMetadata case PngColorType.Rgb: if (this.BitDepth == PngBitDepth.Bit16) { - bpp = 24; + bpp = 48; colorType = PixelColorType.RGB; - info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + info = PixelComponentInfo.Create(3, bpp, 16, 16, 16); break; } - bpp = 48; + bpp = 24; colorType = PixelColorType.RGB; - info = PixelComponentInfo.Create(3, bpp, 16, 16, 16); + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); break; case PngColorType.RgbWithAlpha: diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index 92974aade3..5f408fe0b3 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -73,7 +73,7 @@ internal class QoiDecoderCore : IImageDecoderInternals qoiMetadata.Channels = this.header.Channels; qoiMetadata.ColorSpace = this.header.ColorSpace; - return new ImageInfo(pixelType, size, metadata); + return new ImageInfo(size, metadata); } /// diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 47f7eb0f2e..912b82e8e6 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -646,7 +646,6 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals { this.ReadFileHeader(stream); return new ImageInfo( - new PixelTypeInfo(this.fileHeader.PixelDepth), new(this.fileHeader.Width, this.fileHeader.Height), this.metadata); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 63dc623995..a31a29bdd7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -234,7 +234,7 @@ internal class TiffDecoderCore : IImageDecoderInternals int width = GetImageWidth(rootFrameExifProfile); int height = GetImageHeight(rootFrameExifProfile); - return new ImageInfo(new PixelTypeInfo((int)framesMetadata[0].GetTiffMetadata().BitsPerPixel), new(width, height), metadata, framesMetadata); + return new ImageInfo(new(width, height), metadata, framesMetadata); } /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index c29742da5b..8562ecb7df 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -144,7 +144,6 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( - new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), metadata); } diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index c0d1f27ca4..3a2e870176 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -14,40 +14,43 @@ public class ImageInfo /// /// Initializes a new instance of the class. /// - /// The pixel type information. /// The size of the image in px units. /// The image metadata. public ImageInfo( - PixelTypeInfo pixelType, Size size, - ImageMetadata? metadata) - : this(pixelType, size, metadata, null) + ImageMetadata metadata) + : this(size, metadata, null) { } /// /// Initializes a new instance of the class. /// - /// The pixel type information. /// The size of the image in px units. /// The image metadata. /// The collection of image frame metadata. public ImageInfo( - PixelTypeInfo pixelType, Size size, - ImageMetadata? metadata, + ImageMetadata metadata, IReadOnlyList? frameMetadataCollection) { - this.PixelType = pixelType; this.Size = size; - this.Metadata = metadata ?? new ImageMetadata(); - this.FrameMetadataCollection = frameMetadataCollection ?? Array.Empty(); + this.Metadata = metadata; + + // PixelTpe is normally set following decoding + // See ImageDecoder.SetDecoderFormat(Configuration configuration, ImageInfo info). + if (metadata.DecodedImageFormat is not null) + { + this.PixelType = metadata.GetDecodedPixelTypeInfo(); + } + + this.FrameMetadataCollection = frameMetadataCollection ?? []; } /// /// Gets information about the image pixels. /// - public PixelTypeInfo PixelType { get; } + public PixelTypeInfo PixelType { get; internal set; } /// /// Gets the image width in px units. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index b521f8eeed..72b5601721 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Metadata; @@ -212,12 +213,25 @@ public sealed class ImageMetadata : IDeepCloneable /// The . /// public TFormatMetadata CloneFormatMetadata(IImageFormat key) - where TFormatMetadata : class, IFormatMetadata, new() + where TFormatMetadata : class, IFormatMetadata => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); /// public ImageMetadata DeepClone() => new(this); + internal PixelTypeInfo GetDecodedPixelTypeInfo() + { + // None found. Check if we have a decoded format to convert from. + if (this.DecodedImageFormat is not null + && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) + { + return decodedMetadata.GetPixelTypeInfo(); + } + + // This should never happen. + return default; + } + /// TODO: This should be called on save. /// /// Synchronizes the profiles with the current metadata. diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index ffc2a7aefd..94cfe85ee5 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -4,6 +4,7 @@ using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -112,7 +113,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder()); + image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance)); } [Theory] @@ -219,7 +220,7 @@ public class BmpDecoderTests image.DebugSave(provider); if (TestEnvironment.IsWindows) { - image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder()); + image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance)); } } @@ -232,7 +233,7 @@ public class BmpDecoderTests BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; using Image image = provider.GetImage(BmpDecoder.Instance, options); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] @@ -251,7 +252,7 @@ public class BmpDecoderTests BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; using Image image = provider.GetImage(BmpDecoder.Instance, options); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] @@ -298,7 +299,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] @@ -314,7 +315,7 @@ public class BmpDecoderTests // which should be remapped to 255 for RGBA32, but the magick decoder has a value of 191 set. // The total difference without the alpha channel is still: 0.0204% // Exporting the image as PNG with GIMP yields to the same result as the ImageSharp implementation. - image.CompareToOriginal(provider, ImageComparer.TolerantPercentage(6.1f), new MagickReferenceDecoder()); + image.CompareToOriginal(provider, ImageComparer.TolerantPercentage(6.1f), MagickReferenceDecoder.Png); } [Theory] @@ -327,7 +328,7 @@ public class BmpDecoderTests image.DebugSave(provider); // Do not validate. Reference files will fail validation. - image.CompareToOriginal(provider, new MagickReferenceDecoder(false)); + image.CompareToOriginal(provider, new MagickReferenceDecoder(PngFormat.Instance, false)); } [Theory] @@ -347,7 +348,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] @@ -394,7 +395,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] @@ -404,7 +405,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, new MagickReferenceDecoder()); + image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index a0fbc210f8..d68ec47557 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -20,11 +20,11 @@ public class BmpEncoderTests private static BmpEncoder BmpEncoder => new(); public static readonly TheoryData BitsPerPixel = - new() - { - BmpBitsPerPixel.Bit24, - BmpBitsPerPixel.Bit32 - }; + new() + { + BmpBitsPerPixel.Bit24, + BmpBitsPerPixel.Bit32 + }; public static readonly TheoryData RatioFiles = new() @@ -287,7 +287,7 @@ public class BmpEncoderTests provider, extension: "bmp", appendPixelTypeToFileName: false, - decoder: new MagickReferenceDecoder(false)); + decoder: new MagickReferenceDecoder(BmpFormat.Instance, false)); } [Theory] @@ -318,7 +318,7 @@ public class BmpEncoderTests provider, extension: "bmp", appendPixelTypeToFileName: false, - decoder: new MagickReferenceDecoder(false)); + decoder: new MagickReferenceDecoder(BmpFormat.Instance, false)); } [Theory] @@ -380,8 +380,8 @@ public class BmpEncoderTests { using Image image = provider.GetImage(); - using var reencodedStream = new MemoryStream(); - var encoder = new BmpEncoder + using MemoryStream reencodedStream = new(); + BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel, SupportTransparency = false, diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 2fe4282607..69f008e522 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg; [ValidateDisposedMemoryAllocations] public partial class JpegDecoderTests { - private static MagickReferenceDecoder ReferenceDecoder => new(); + private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Jpeg; public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index a39e3a8b99..9bd78b8f2e 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -485,7 +485,7 @@ public partial class PngDecoderTests if (compare) { // Magick cannot actually decode this image to compare. - image.CompareToOriginal(provider, new MagickReferenceDecoder(false)); + image.CompareToOriginal(provider, new MagickReferenceDecoder(PngFormat.Instance, false)); } } @@ -552,7 +552,7 @@ public partial class PngDecoderTests // We don't have another x-plat reference decoder that can be compared for this image. if (TestEnvironment.IsWindows) { - image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Instance); + image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Png); } }); Assert.Null(ex); @@ -614,7 +614,7 @@ public partial class PngDecoderTests // We don't have another x-plat reference decoder that can be compared for this image. if (TestEnvironment.IsWindows) { - image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Instance); + image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Png); } }); Assert.NotNull(ex); diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs index d57b597b06..32ade4a1e9 100644 --- a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs @@ -24,7 +24,7 @@ public class QoiEncoderTests public static void Encode(TestImageProvider provider, QoiChannels channels, QoiColorSpace colorSpace) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(new MagickReferenceDecoder()); + using Image image = provider.GetImage(new MagickReferenceDecoder(QoiFormat.Instance)); using MemoryStream stream = new(); QoiEncoder encoder = new() { @@ -34,7 +34,7 @@ public class QoiEncoderTests image.Save(stream, encoder); stream.Position = 0; - using Image encodedImage = (Image)Image.Load(stream); + using Image encodedImage = Image.Load(stream); QoiMetadata qoiMetadata = encodedImage.Metadata.GetQoiMetadata(); ImageComparer.Exact.CompareImages(image, encodedImage); diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index c9f93446f4..615e0fc921 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -121,16 +121,14 @@ public class TgaEncoderTests where TPixel : unmanaged, IPixel { // The test image has alternating black and white pixels, which should make using always RLE data inefficient. - using (Image image = provider.GetImage()) - { - var options = new TgaEncoder() { Compression = TgaCompression.RunLength }; - using (var memStream = new MemoryStream()) - { - image.Save(memStream, options); - byte[] imageBytes = memStream.ToArray(); - Assert.Equal(expectedBytes, imageBytes.Length); - } - } + using Image image = provider.GetImage(); + TgaEncoder options = new() { BitsPerPixel = TgaBitsPerPixel.Bit24, Compression = TgaCompression.RunLength }; + + using MemoryStream memStream = new(); + image.Save(memStream, options); + byte[] imageBytes = memStream.ToArray(); + + Assert.Equal(expectedBytes, imageBytes.Length); } // Run length encoded pixels should not exceed row boundaries. diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs index 4acdf3e509..b574c7e55c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff; public abstract class TiffDecoderBaseTester { - protected static MagickReferenceDecoder ReferenceDecoder => new(); + protected static MagickReferenceDecoder ReferenceDecoder => new(TiffFormat.Instance); protected static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) where TPixel : unmanaged, IPixel diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs index 0cff352171..1bf9f5a400 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff; [Trait("Format", "Tiff")] public abstract class TiffEncoderBaseTester { - protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(TiffFormat.Instance); protected static void TestStripLength( TestImageProvider provider, @@ -48,7 +48,6 @@ public abstract class TiffEncoderBaseTester Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; Assert.NotNull(stripByteCounts); Assert.True(stripByteCounts.Length > 1); - Assert.NotNull(outputMeta.BitsPerPixel); foreach (Number sz in stripByteCounts) { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 99f4dee168..657ab25546 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp; [ValidateDisposedMemoryAllocations] public class WebpDecoderTests { - private static MagickReferenceDecoder ReferenceDecoder => new(); + private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.WebP; private static string TestImageLossyHorizontalFilterPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.AlphaCompressedHorizontalFilter); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index aff1b35946..5d09b37093 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -169,10 +169,11 @@ public class WebpEncoderTests } [Theory] - [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. + [WithFile(TestImages.Jpeg.Baseline.Jpeg410, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // Input is lossy jpeg. + [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] // Input is lossless png. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] [WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] - public void Encode_PreserveRatio(TestImageProvider provider, WebpFileFormatType expectedFormat) + public void Encode_PreserveEncodingType(TestImageProvider provider, WebpFileFormatType expectedFormat) where TPixel : unmanaged, IPixel { WebpEncoder options = new(); @@ -440,7 +441,7 @@ public class WebpEncoderTests "with_alpha", encoder, ImageComparer.Tolerant(0.04f), - referenceDecoder: new MagickReferenceDecoder()); + referenceDecoder: MagickReferenceDecoder.WebP); int encodedBytes = File.ReadAllBytes(encodedFile).Length; Assert.True(encodedBytes <= expectedFileSize, $"encoded bytes are {encodedBytes} and should be smaller then expected file size of {expectedFileSize}"); @@ -468,7 +469,7 @@ public class WebpEncoderTests "with_alpha_compressed", encoder, ImageComparer.Tolerant(0.04f), - referenceDecoder: new MagickReferenceDecoder()); + referenceDecoder: MagickReferenceDecoder.WebP); int encodedBytes = File.ReadAllBytes(encodedFile).Length; Assert.True(encodedBytes <= expectedFileSize, $"encoded bytes are {encodedBytes} and should be smaller then expected file size of {expectedFileSize}"); diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 3ae6601b18..f50bc89335 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp; [Trait("Format", "Webp")] public class YuvConversionTests { - private static MagickReferenceDecoder ReferenceDecoder => new(); + private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.WebP; private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Webp.Lossy.NoFilter06); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index 996310d8c3..51e2a01a28 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -4,6 +4,7 @@ using Moq; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -57,7 +58,7 @@ public partial class ImageTests // TODO: Remove all this mocking. It's too complicated and we can now use fakes. this.localStreamReturnImageRgba32 = new Image(1, 1); this.localStreamReturnImageAgnostic = new Image(1, 1); - this.LocalImageInfo = new(new PixelTypeInfo(8), new(1, 1), new ImageMetadata()); + this.LocalImageInfo = new(new(1, 1), new ImageMetadata() { DecodedImageFormat = PngFormat.Instance }); this.localImageFormatMock = new Mock(); diff --git a/tests/ImageSharp.Tests/ImageInfoTests.cs b/tests/ImageSharp.Tests/ImageInfoTests.cs index 576d14396e..322b0af196 100644 --- a/tests/ImageSharp.Tests/ImageInfoTests.cs +++ b/tests/ImageSharp.Tests/ImageInfoTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests; @@ -15,12 +15,14 @@ public class ImageInfoTests const int height = 60; Size size = new(width, height); Rectangle rectangle = new(0, 0, width, height); - PixelTypeInfo pixelType = new(8); - ImageMetadata meta = new(); - ImageInfo info = new(pixelType, size, meta); + // Initialize the metadata to match standard decoding behavior. + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); - Assert.Equal(pixelType, info.PixelType); + ImageInfo info = new(size, meta); + + Assert.NotEqual(default, info.PixelType); Assert.Equal(width, info.Width); Assert.Equal(height, info.Height); Assert.Equal(size, info.Size); @@ -35,13 +37,16 @@ public class ImageInfoTests const int height = 60; Size size = new(width, height); Rectangle rectangle = new(0, 0, width, height); - PixelTypeInfo pixelType = new(8); - ImageMetadata meta = new(); - IReadOnlyList frameMetadata = new List() { new() }; - ImageInfo info = new(pixelType, size, meta, frameMetadata); + // Initialize the metadata to match standard decoding behavior. + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); + + IReadOnlyList frameMetadata = [new()]; + + ImageInfo info = new(size, meta, frameMetadata); - Assert.Equal(pixelType, info.PixelType); + Assert.NotEqual(default, info.PixelType); Assert.Equal(width, info.Width); Assert.Equal(height, info.Height); Assert.Equal(size, info.Size); diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index 10211f386c..d30ce7846d 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests; @@ -89,7 +90,7 @@ public class TestFormat : IImageFormatConfigurationModule, IImageFormat { if (!this.sampleImages.ContainsKey(typeof(TPixel))) { - this.sampleImages.Add(typeof(TPixel), new Image(1, 1)); + this.sampleImages.Add(typeof(TPixel), ReferenceCodecUtilities.EnsureDecodedMetadata(new Image(1, 1), this)); } return (Image)this.sampleImages[typeof(TPixel)]; @@ -202,11 +203,12 @@ public class TestFormat : IImageFormatConfigurationModule, IImageFormat protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { - Image image = - this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - ImageFrameCollection m = image.Frames; - - return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata))); + using Image image = this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + ImageMetadata metadata = image.Metadata; + return new(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) + { + PixelType = metadata.GetDecodedPixelTypeInfo() + }; } protected override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 80b5536ebd..f96dc19ee0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -5,6 +5,11 @@ using System.Runtime.InteropServices; using ImageMagick; using ImageMagick.Formats; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -13,19 +18,34 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; public class MagickReferenceDecoder : ImageDecoder { + private readonly IImageFormat imageFormat; private readonly bool validate; - public MagickReferenceDecoder() - : this(true) + public MagickReferenceDecoder(IImageFormat imageFormat) + : this(imageFormat, true) { } - public MagickReferenceDecoder(bool validate) => this.validate = validate; + public MagickReferenceDecoder(IImageFormat imageFormat, bool validate) + { + this.imageFormat = imageFormat; + this.validate = validate; + } + + public static MagickReferenceDecoder Png { get; } = new MagickReferenceDecoder(PngFormat.Instance); + + public static MagickReferenceDecoder Bmp { get; } = new MagickReferenceDecoder(BmpFormat.Instance); + + public static MagickReferenceDecoder Jpeg { get; } = new MagickReferenceDecoder(JpegFormat.Instance); - public static MagickReferenceDecoder Instance { get; } = new(); + public static MagickReferenceDecoder Tiff { get; } = new MagickReferenceDecoder(TiffFormat.Instance); + + public static MagickReferenceDecoder WebP { get; } = new MagickReferenceDecoder(WebpFormat.Instance); protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { + ImageMetadata metadata = new(); + Configuration configuration = options.Configuration; BmpReadDefines bmpReadDefines = new() { @@ -44,7 +64,7 @@ public class MagickReferenceDecoder : ImageDecoder settings.SetDefines(pngReadDefines); using MagickImageCollection magickImageCollection = new(stream, settings); - List> framesList = new(); + List> framesList = []; foreach (IMagickImage magicFrame in magickImageCollection) { ImageFrame frame = new(configuration, magicFrame.Width, magicFrame.Height); @@ -61,6 +81,11 @@ public class MagickReferenceDecoder : ImageDecoder } else if (magicFrame.Depth is 16 or 14) { + if (this.imageFormat is PngFormat png) + { + metadata.GetPngMetadata().BitDepth = PngBitDepth.Bit16; + } + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); FromRgba64Bytes(configuration, bytes, framePixels); @@ -71,7 +96,7 @@ public class MagickReferenceDecoder : ImageDecoder } } - return new Image(configuration, new ImageMetadata(), framesList); + return ReferenceCodecUtilities.EnsureDecodedMetadata(new(configuration, metadata, framesList), this.imageFormat); } protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -80,7 +105,11 @@ public class MagickReferenceDecoder : ImageDecoder protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { using Image image = this.Decode(options, stream, cancellationToken); - return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata))); + ImageMetadata metadata = image.Metadata; + return new(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) + { + PixelType = metadata.GetDecodedPixelTypeInfo() + }; } private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs new file mode 100644 index 0000000000..e48116fed3 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +internal static class ReferenceCodecUtilities +{ + /// + /// Ensures that the metadata is properly initialized for reference and test encoders which cannot initialize + /// metadata in the same manner as our built in decoders. + /// + /// The type of pixel format. + /// The decoded image. + /// The image format + /// The format is unknown. + public static Image EnsureDecodedMetadata(Image image, IImageFormat format) + where TPixel : unmanaged, IPixel + { + if (image.Metadata.DecodedImageFormat is null) + { + image.Metadata.DecodedImageFormat = format; + } + + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.DecodedImageFormat = format; + } + + switch (format) + { + case BmpFormat: + image.Metadata.GetBmpMetadata(); + break; + case GifFormat: + image.Metadata.GetGifMetadata(); + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.GetGifMetadata(); + } + + break; + case JpegFormat: + image.Metadata.GetJpegMetadata(); + break; + case PbmFormat: + image.Metadata.GetPbmMetadata(); + break; + case PngFormat: + image.Metadata.GetPngMetadata(); + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.GetPngMetadata(); + } + + break; + case QoiFormat: + image.Metadata.GetQoiMetadata(); + break; + case TgaFormat: + image.Metadata.GetTgaMetadata(); + break; + case TiffFormat: + image.Metadata.GetTiffMetadata(); + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.GetTiffMetadata(); + } + + break; + case WebpFormat: + image.Metadata.GetWebpMetadata(); + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.GetWebpMetadata(); + } + + break; + } + + return image; + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index a3408bedb4..14a655823d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -1,23 +1,35 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable CA1416 // Validate platform compatibility using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SDBitmap = System.Drawing.Bitmap; -using SDImage = System.Drawing.Image; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; public class SystemDrawingReferenceDecoder : ImageDecoder { - public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); + private readonly IImageFormat imageFormat; + + public SystemDrawingReferenceDecoder(IImageFormat imageFormat) + => this.imageFormat = imageFormat; + + public static SystemDrawingReferenceDecoder Png { get; } = new SystemDrawingReferenceDecoder(PngFormat.Instance); + + public static SystemDrawingReferenceDecoder Bmp { get; } = new SystemDrawingReferenceDecoder(BmpFormat.Instance); protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { - using SDBitmap sourceBitmap = new(stream); - PixelTypeInfo pixelType = new(SDImage.GetPixelFormatSize(sourceBitmap.PixelFormat)); - return new ImageInfo(pixelType, new(sourceBitmap.Width, sourceBitmap.Height), new ImageMetadata()); + using Image image = this.Decode(options, stream, cancellationToken); + ImageMetadata metadata = image.Metadata; + return new(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) + { + PixelType = metadata.GetDecodedPixelTypeInfo() + }; } protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -42,7 +54,9 @@ public class SystemDrawingReferenceDecoder : ImageDecoder g.DrawImage(sourceBitmap, 0, 0, sourceBitmap.Width, sourceBitmap.Height); } - return SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(convertedBitmap); + return ReferenceCodecUtilities.EnsureDecodedMetadata( + SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(convertedBitmap), + this.imageFormat); } protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs index d8dda2eea8..0789ab2634 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable CA1416 // Validate platform compatibility using System.Drawing.Imaging; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 9508de2469..e4bee955b9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -71,13 +71,13 @@ public static partial class TestEnvironment // Magick codecs should work on all platforms cfg.ConfigureCodecs( PngFormat.Instance, - MagickReferenceDecoder.Instance, + MagickReferenceDecoder.Png, pngEncoder, new PngImageFormatDetector()); cfg.ConfigureCodecs( BmpFormat.Instance, - IsWindows ? SystemDrawingReferenceDecoder.Instance : MagickReferenceDecoder.Instance, + IsWindows ? SystemDrawingReferenceDecoder.Bmp : MagickReferenceDecoder.Bmp, bmpEncoder, new BmpImageFormatDetector()); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs index 81ea77b6b6..b818e80b05 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs @@ -7,14 +7,13 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit.Abstractions; -// ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests; public class MagickReferenceCodecTests { public MagickReferenceCodecTests(ITestOutputHelper output) => this.Output = output; - private ITestOutputHelper Output { get; } + public ITestOutputHelper Output { get; } public const PixelTypes PixelTypesToTest32 = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24; @@ -32,8 +31,8 @@ public class MagickReferenceCodecTests { string path = TestFile.GetInputFileFullPath(testImage); - var magickDecoder = new MagickReferenceDecoder(); - var sdDecoder = new SystemDrawingReferenceDecoder(); + MagickReferenceDecoder magickDecoder = MagickReferenceDecoder.Png; + SystemDrawingReferenceDecoder sdDecoder = SystemDrawingReferenceDecoder.Png; ImageComparer comparer = ImageComparer.Exact; @@ -64,11 +63,11 @@ public class MagickReferenceCodecTests { string path = TestFile.GetInputFileFullPath(testImage); - var magickDecoder = new MagickReferenceDecoder(); - var sdDecoder = new SystemDrawingReferenceDecoder(); + MagickReferenceDecoder magickDecoder = MagickReferenceDecoder.Png; + SystemDrawingReferenceDecoder sdDecoder = SystemDrawingReferenceDecoder.Png; - // 1020 == 4 * 255 (Equivalent to manhattan distance of 1+1+1+1=4 in Rgba32 space) - var comparer = ImageComparer.TolerantPercentage(1, 1020); + // 1020 == 4 * 255 (Equivalent to Manhattan distance of 1+1+1+1=4 in Rgba32 space) + ImageComparer comparer = ImageComparer.TolerantPercentage(1, 1020); using FileStream mStream = File.OpenRead(path); using FileStream sdStream = File.OpenRead(path); using Image mImage = magickDecoder.Decode(DecoderOptions.Default, mStream); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs index 3a3fcefdbe..6e1eba28e8 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs @@ -47,7 +47,7 @@ public class ReferenceDecoderBenchmarks public void BenchmarkMagickPngDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel { - this.BenchmarkDecoderImpl(PngBenchmarkFiles, new MagickReferenceDecoder(), "Magick Decode Png"); + this.BenchmarkDecoderImpl(PngBenchmarkFiles, MagickReferenceDecoder.Png, "Magick Decode Png"); } [Theory(Skip = SkipBenchmarks)] @@ -55,7 +55,7 @@ public class ReferenceDecoderBenchmarks public void BenchmarkSystemDrawingPngDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel { - this.BenchmarkDecoderImpl(PngBenchmarkFiles, new SystemDrawingReferenceDecoder(), "System.Drawing Decode Png"); + this.BenchmarkDecoderImpl(PngBenchmarkFiles, SystemDrawingReferenceDecoder.Png, "System.Drawing Decode Png"); } [Theory(Skip = SkipBenchmarks)] @@ -63,7 +63,7 @@ public class ReferenceDecoderBenchmarks public void BenchmarkMagickBmpDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel { - this.BenchmarkDecoderImpl(BmpBenchmarkFiles, new MagickReferenceDecoder(), "Magick Decode Bmp"); + this.BenchmarkDecoderImpl(BmpBenchmarkFiles, MagickReferenceDecoder.Bmp, "Magick Decode Bmp"); } [Theory(Skip = SkipBenchmarks)] @@ -71,7 +71,7 @@ public class ReferenceDecoderBenchmarks public void BenchmarkSystemDrawingBmpDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel { - this.BenchmarkDecoderImpl(BmpBenchmarkFiles, new SystemDrawingReferenceDecoder(), "System.Drawing Decode Bmp"); + this.BenchmarkDecoderImpl(BmpBenchmarkFiles, SystemDrawingReferenceDecoder.Bmp, "System.Drawing Decode Bmp"); } private void BenchmarkDecoderImpl(IEnumerable testFiles, IImageDecoder decoder, string info, int times = DefaultExecutionCount) diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs index a89feb3c35..4608583791 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs @@ -93,7 +93,7 @@ public class SystemDrawingReferenceCodecTests { string path = TestFile.GetInputFileFullPath(TestImages.Png.Splash); using FileStream stream = File.OpenRead(path); - using Image image = SystemDrawingReferenceDecoder.Instance.Decode(DecoderOptions.Default, stream); + using Image image = SystemDrawingReferenceDecoder.Png.Decode(DecoderOptions.Default, stream); image.DebugSave(dummyProvider); } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 2f5291131c..f33811206a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -3,9 +3,11 @@ using System.Collections.Concurrent; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit.Abstractions; // ReSharper disable InconsistentNaming @@ -25,7 +27,7 @@ public class TestImageProviderTests TestImageProvider.File(TestImages.Bmp.F) }; - public static string[] AllBmpFiles = { TestImages.Bmp.F, TestImages.Bmp.Bit8 }; + public static string[] AllBmpFiles = [TestImages.Bmp.F, TestImages.Bmp.Bit8]; public TestImageProviderTests(ITestOutputHelper output) => this.Output = output; @@ -80,9 +82,9 @@ public class TestImageProviderTests TestDecoder.DoTestThreadSafe( () => { - string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache); + const string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache); - var decoder = new TestDecoder(); + TestDecoder decoder = new(); decoder.InitCaller(testName); provider.GetImage(decoder); @@ -335,7 +337,7 @@ public class TestImageProviderTests { using (provider.GetImage()) { - var customConfiguration = Configuration.CreateDefaultInstance(); + Configuration customConfiguration = Configuration.CreateDefaultInstance(); provider.Configuration = customConfiguration; using Image image2 = provider.GetImage(); @@ -365,13 +367,17 @@ public class TestImageProviderTests protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { using Image image = this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata))); + ImageMetadata metadata = image.Metadata; + return new(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) + { + PixelType = metadata.GetDecodedPixelTypeInfo() + }; } protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) { InvocationCounts[this.callerName]++; - return new Image(42, 42); + return ReferenceCodecUtilities.EnsureDecodedMetadata(new Image(42, 42), PngFormat.Instance); } protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -408,13 +414,17 @@ public class TestImageProviderTests protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { using Image image = this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata))); + ImageMetadata metadata = image.Metadata; + return new(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) + { + PixelType = metadata.GetDecodedPixelTypeInfo() + }; } protected override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) { InvocationCounts[this.callerName]++; - return new Image(42, 42); + return ReferenceCodecUtilities.EnsureDecodedMetadata(new Image(42, 42), PngFormat.Instance); } protected override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) From 8b5703c91670d3c716c3b23f0d4c946c4f0f1df3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 13 Jun 2024 18:23:52 +1000 Subject: [PATCH 25/43] Synchronize profiles on save. --- src/ImageSharp/Formats/ImageEncoder.cs | 7 +++-- .../Formats/Jpeg/JpegEncoderCore.cs | 6 ---- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 1 - .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 -- .../Formats/Webp/Lossy/Vp8Encoder.cs | 2 -- src/ImageSharp/Image.cs | 22 ++++++++++++++ src/ImageSharp/Metadata/ImageFrameMetadata.cs | 5 ++++ src/ImageSharp/Metadata/ImageMetadata.cs | 11 ++++--- .../Metadata/Profiles/Exif/ExifProfile.cs | 11 +++++++ .../Metadata/ImageMetadataTests.cs | 29 +++++++++---------- 10 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index 4acd29e81c..fdaa5c35dc 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; @@ -42,6 +41,8 @@ public abstract class ImageEncoder : IImageEncoder private void EncodeWithSeekableStream(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + image.SynchronizeMetadata(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { @@ -59,6 +60,8 @@ public abstract class ImageEncoder : IImageEncoder private async Task EncodeWithSeekableStreamAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + image.SynchronizeMetadata(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { @@ -66,7 +69,7 @@ public abstract class ImageEncoder : IImageEncoder } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 523d6b3ba8..fbc9286068 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -539,17 +539,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// Temporary buffer. private void WriteProfiles(ImageMetadata metadata, Span buffer) { - if (metadata is null) - { - return; - } - // For compatibility, place the profiles in the following order: // - APP1 EXIF // - APP1 XMP // - APP2 ICC // - APP13 IPTC - metadata.SyncProfiles(); this.WriteExifProfile(metadata.ExifProfile, buffer); this.WriteXmpProfile(metadata.XmpProfile, buffer); this.WriteIccProfile(metadata.IccProfile, buffer); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a5ab739882..9f2958272d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -776,7 +776,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable return; } - meta.SyncProfiles(); this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 58d007265b..57f744e41f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -241,8 +241,6 @@ internal class Vp8LEncoder : IDisposable { // Write bytes from the bit-writer buffer to the stream. ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 7e1ef93cfe..3ad72f7d00 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -315,8 +315,6 @@ internal class Vp8Encoder : IDisposable { // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 03a19a4be8..d4f773abe1 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -157,6 +157,28 @@ public abstract partial class Image : IDisposable, IConfigurationProvider public abstract Image CloneAs(Configuration configuration) where TPixel2 : unmanaged, IPixel; + /// + /// Synchronizes any embedded metadata profiles with the current image properties. + /// + public void SynchronizeMetadata() + { + this.Metadata.SynchronizeProfiles(); + foreach (ImageFrame frame in this.Frames) + { + frame.Metadata.SynchronizeProfiles(); + } + } + + /// + /// Synchronizes any embedded metadata profiles with the current image properties. + /// + /// A synchronization action to run in addition to the default process. + public void SynchronizeMetadata(Action action) + { + this.SynchronizeMetadata(); + action(this); + } + /// /// Update the size of the image after mutation. /// diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index afda71879f..bb78f3b3e5 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -131,4 +131,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable where TFormatMetadata : class where TFormatFrameMetadata : class, IFormatFrameMetadata => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); + + /// + /// Synchronizes the profiles with the current metadata. + /// + internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 72b5601721..3711da96df 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -219,6 +219,11 @@ public sealed class ImageMetadata : IDeepCloneable /// public ImageMetadata DeepClone() => new(this); + /// + /// Synchronizes the profiles with the current metadata. + /// + internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + internal PixelTypeInfo GetDecodedPixelTypeInfo() { // None found. Check if we have a decoded format to convert from. @@ -231,10 +236,4 @@ public sealed class ImageMetadata : IDeepCloneable // This should never happen. return default; } - - /// TODO: This should be called on save. - /// - /// Synchronizes the profiles with the current metadata. - /// - internal void SyncProfiles() => this.ExifProfile?.Sync(this); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index dd5792ae79..41d3c293b6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -298,6 +298,17 @@ public sealed class ExifProfile : IDeepCloneable this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); } + /// + /// Synchronizes the profiles with the specified metadata. + /// + /// The metadata. +#pragma warning disable CA1822, RCS1163, IDE0060 + internal void Sync(ImageFrameMetadata metadata) +#pragma warning restore IDE0060, RCS1163, CA1822 + { + // Nothing to do ....YET. + } + private void SyncResolution(ExifTag tag, double resolution) { if (!this.TryGetValue(tag, out IExifValue? value)) diff --git a/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs index 330b701474..ae02c3d57b 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs @@ -16,9 +16,9 @@ public class ImageMetadataTests [Fact] public void ConstructorImageMetadata() { - var metaData = new ImageMetadata(); + ImageMetadata metaData = new(); - var exifProfile = new ExifProfile(); + ExifProfile exifProfile = new(); metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; @@ -34,7 +34,7 @@ public class ImageMetadataTests [Fact] public void CloneIsDeep() { - var metaData = new ImageMetadata + ImageMetadata metaData = new() { ExifProfile = new ExifProfile(), HorizontalResolution = 4, @@ -53,7 +53,7 @@ public class ImageMetadataTests [Fact] public void HorizontalResolution() { - var metaData = new ImageMetadata(); + ImageMetadata metaData = new(); Assert.Equal(96, metaData.HorizontalResolution); metaData.HorizontalResolution = 0; @@ -69,7 +69,7 @@ public class ImageMetadataTests [Fact] public void VerticalResolution() { - var metaData = new ImageMetadata(); + ImageMetadata metaData = new(); Assert.Equal(96, metaData.VerticalResolution); metaData.VerticalResolution = 0; @@ -85,20 +85,19 @@ public class ImageMetadataTests [Fact] public void SyncProfiles() { - var exifProfile = new ExifProfile(); + ExifProfile exifProfile = new(); exifProfile.SetValue(ExifTag.XResolution, new Rational(200)); exifProfile.SetValue(ExifTag.YResolution, new Rational(300)); - using (var image = new Image(1, 1)) - { - image.Metadata.ExifProfile = exifProfile; - image.Metadata.HorizontalResolution = 400; - image.Metadata.VerticalResolution = 500; + using Image image = new(1, 1); + image.Metadata.ExifProfile = exifProfile; + image.Metadata.HorizontalResolution = 400; + image.Metadata.VerticalResolution = 500; - image.Metadata.SyncProfiles(); + using MemoryStream memoryStream = new(); + image.SaveAsBmp(memoryStream); - Assert.Equal(400, image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); - Assert.Equal(500, image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); - } + Assert.Equal(400, image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(500, image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); } } From 9bdbc526da7a13e62f22677d71f316867a95f078 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 19 Jun 2024 22:59:39 +1000 Subject: [PATCH 26/43] Migrate ICO and CUR --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 3 + src/ImageSharp/Formats/Cur/CurDecoderCore.cs | 18 +- .../Formats/Cur/CurFrameMetadata.cs | 133 ++++++++++- src/ImageSharp/Formats/Cur/CurMetadata.cs | 173 ++++++++++++++- .../Formats/Cur/MetadataExtensions.cs | 45 ---- .../Formats/FormatConnectingFrameMetadata.cs | 39 +++- .../Formats/FormatConnectingMetadata.cs | 2 +- src/ImageSharp/Formats/Ico/IcoDecoderCore.cs | 16 +- .../Formats/Ico/IcoFrameMetadata.cs | 133 ++++++++++- src/ImageSharp/Formats/Ico/IcoMetadata.cs | 159 +++++++++++++- .../Formats/Ico/MetadataExtensions.cs | 45 ---- .../Formats/Icon/IconDecoderCore.cs | 14 +- .../Formats/Icon/IconEncoderCore.cs | 2 +- .../_Generated/ImageExtensions.Save.cs | 206 ++++++++++++++++++ .../_Generated/ImageMetadataExtensions.cs | 82 +++++++ .../Formats/_Generated/_Formats.ttinclude | 4 + src/ImageSharp/Metadata/ImageFrameMetadata.cs | 6 +- src/ImageSharp/Metadata/ImageMetadata.cs | 6 +- .../Formats/Icon/Cur/CurDecoderTests.cs | 4 +- .../Formats/Icon/Cur/CurEncoderTests.cs | 34 ++- .../Formats/Icon/Ico/IcoDecoderTests.cs | 16 +- .../Formats/Icon/Ico/IcoEncoderTests.cs | 33 ++- 22 files changed, 1034 insertions(+), 139 deletions(-) delete mode 100644 src/ImageSharp/Formats/Cur/MetadataExtensions.cs delete mode 100644 src/ImageSharp/Formats/Ico/MetadataExtensions.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 707dd34c48..68e99bdc5f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -143,6 +143,9 @@ public class BmpMetadata : IFormatMetadata public FormatConnectingMetadata ToFormatConnectingMetadata() => new() { + EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, PixelTypeInfo = this.GetPixelTypeInfo() }; diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index 3018ec6bf9..a8a51878e0 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -15,16 +15,30 @@ internal sealed class CurDecoderCore : IconDecoderCore } protected override void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, ReadOnlyMemory? colorTable) { - CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); + CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); curFrameMetadata.FromIconDirEntry(entry); curFrameMetadata.Compression = compression; curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; curFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + CurMetadata curMetadata = imageMetadata.GetCurMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; + curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; + curMetadata.HotspotX = curFrameMetadata.HotspotX; + curMetadata.HotspotY = curFrameMetadata.HotspotY; + } } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 014944ba69..06cf426dc4 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// /// IcoFrameMetadata. /// -public class CurFrameMetadata : IDeepCloneable, IDeepCloneable +public class CurFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -60,7 +60,7 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable /// Gets or sets the number of bits per pixel.
/// Used when is ///
- public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// /// Gets or sets the color table, if any. @@ -69,11 +69,75 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable public ReadOnlyMemory? ColorTable { get; set; } /// - public CurFrameMetadata DeepClone() => new(this); + public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new CurFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + byte encodingWidth = metadata.EncodingWidth switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingWidth, + _ => 0 + }; + + byte encodingHeight = metadata.EncodingHeight switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingHeight, + _ => 0 + }; + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = encodingWidth, + EncodingHeight = encodingHeight, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable, + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + /// + public CurFrameMetadata DeepClone() => new(this); + internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; @@ -84,7 +148,7 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable internal IconDirEntry ToIconDirEntry() { - byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); @@ -97,4 +161,65 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable ColorCount = colorCount }; } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } } diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index 5c3486d4a2..6e97a8584a 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs @@ -1,16 +1,183 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Provides Ico specific metadata information for the image. +/// Provides Cur specific metadata information for the image. /// -public class CurMetadata : IDeepCloneable, IDeepCloneable +public class CurMetadata : IFormatMetadata { + /// + /// Initializes a new instance of the class. + /// + public CurMetadata() + { + } + + private CurMetadata(CurMetadata other) + { + this.Compression = other.Compression; + this.HotspotX = other.HotspotX; + this.HotspotY = other.HotspotY; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame. + /// + public ushort HotspotX { get; set; } + + /// + /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame. + /// + public ushort HotspotY { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ 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 or greater. Derived from the root frame. + ///
+ public byte EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + /// - public CurMetadata DeepClone() => new(); + public static CurMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public CurMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Cur/MetadataExtensions.cs b/src/ImageSharp/Formats/Cur/MetadataExtensions.cs deleted file mode 100644 index 6394c564b0..0000000000 --- a/src/ImageSharp/Formats/Cur/MetadataExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Cur; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the Icon format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static CurMetadata GetCurMetadata(this ImageMetadata source) - => source.GetFormatMetadata(CurFormat.Instance); - - /// - /// Gets the Icon format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) - => source.GetFormatMetadata(CurFormat.Instance); - - /// - /// Gets the Icon format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified frame, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the Icon frame metadata exists; otherwise, . - /// - public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata) - => source.TryGetFormatMetadata(CurFormat.Instance, out metadata); -} diff --git a/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs index 31555afe34..ded220c9ad 100644 --- a/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats; /// @@ -9,27 +11,44 @@ namespace SixLabors.ImageSharp.Formats; public class FormatConnectingFrameMetadata { /// - /// Gets or sets the frame color table. + /// Gets information about the encoded pixel type if any. + /// + public PixelTypeInfo? PixelTypeInfo { get; init; } + + /// + /// Gets the frame color table if any. + /// + public ReadOnlyMemory? ColorTable { get; init; } + + /// + /// Gets the frame color table mode. + /// + public FrameColorTableMode ColorTableMode { get; init; } + + /// + /// Gets the duration of the frame. /// - public ReadOnlyMemory? ColorTable { get; set; } + public TimeSpan Duration { get; init; } /// - /// Gets or sets the frame color table mode. + /// Gets the frame alpha blending mode. /// - public FrameColorTableMode ColorTableMode { get; set; } + public FrameBlendMode BlendMode { get; init; } /// - /// Gets or sets the duration of the frame. + /// Gets the frame disposal mode. /// - public TimeSpan Duration { get; set; } + public FrameDisposalMode DisposalMode { get; init; } /// - /// Gets or sets the frame alpha blending mode. + /// Gets or sets the encoding width.
+ /// Used for formats that require a specific frame size. ///
- public FrameBlendMode BlendMode { get; set; } + public int? EncodingWidth { get; set; } /// - /// Gets or sets the frame disposal mode. + /// Gets or sets the encoding height.
+ /// Used for formats that require a specific frame size. ///
- public FrameDisposalMode DisposalMode { get; set; } + public int? EncodingHeight { get; set; } } diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index 07579c09e8..baf0a35457 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -29,7 +29,7 @@ public class FormatConnectingMetadata public PixelTypeInfo PixelTypeInfo { get; init; } /// - /// Gets the shared color table. + /// Gets the shared color table if any. /// public ReadOnlyMemory? ColorTable { get; init; } diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index f4990c66af..8b59974eb3 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -15,16 +15,28 @@ internal sealed class IcoDecoderCore : IconDecoderCore } protected override void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, ReadOnlyMemory? colorTable) { - IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); + IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); icoFrameMetadata.FromIconDirEntry(entry); icoFrameMetadata.Compression = compression; icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; icoFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + IcoMetadata curMetadata = imageMetadata.GetIcoMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; + curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; + } } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index ea27d13c8d..c244e38981 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// /// Provides Ico specific metadata information for the image frame. /// -public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable +public class IcoFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -53,7 +53,7 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable /// Gets or sets the number of bits per pixel.
/// Used when is ///
- public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// /// Gets or sets the color table, if any. @@ -62,11 +62,75 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable public ReadOnlyMemory? ColorTable { get; set; } /// - public IcoFrameMetadata DeepClone() => new(this); + public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new IcoFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + byte encodingWidth = metadata.EncodingWidth switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingWidth, + _ => 0 + }; + + byte encodingHeight = metadata.EncodingHeight switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingHeight, + _ => 0 + }; + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = encodingWidth, + EncodingHeight = encodingHeight, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable, + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + /// + public IcoFrameMetadata DeepClone() => new(this); + internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; @@ -75,7 +139,7 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable internal IconDirEntry ToIconDirEntry() { - byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); @@ -92,4 +156,65 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable }, }; } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } } diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index f165bf9164..7e31468ecc 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -1,16 +1,171 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Ico; /// /// Provides Ico specific metadata information for the image. /// -public class IcoMetadata : IDeepCloneable, IDeepCloneable +public class IcoMetadata : IFormatMetadata { + /// + /// Initializes a new instance of the class. + /// + public IcoMetadata() + { + } + + private IcoMetadata(IcoMetadata other) + { + 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(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ 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 or greater. Derived from the root frame. + ///
+ public byte EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static IcoMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + /// - public IcoMetadata DeepClone() => new(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public IcoMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Ico/MetadataExtensions.cs b/src/ImageSharp/Formats/Ico/MetadataExtensions.cs deleted file mode 100644 index 497375f994..0000000000 --- a/src/ImageSharp/Formats/Ico/MetadataExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Ico; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the Ico format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static IcoMetadata GetIcoMetadata(this ImageMetadata source) - => source.GetFormatMetadata(IcoFormat.Instance); - - /// - /// Gets the Ico format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) - => source.GetFormatMetadata(IcoFormat.Instance); - - /// - /// Gets the Ico format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified frame, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the Ico frame metadata exists; otherwise, . - /// - public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata) - => source.TryGetFormatMetadata(IcoFormat.Instance, out metadata); -} diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index 74fe7b9e50..0feed7f693 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -74,7 +74,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals PngMetadata? pngMetadata = null; Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; ReadOnlyMemory? colorTable = null; ImageFrame target = new(this.Options.Configuration, this.Dimensions); ImageFrame source = x.Image.Frames.RootFrameUnsafe; @@ -106,7 +106,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals } this.SetFrameMetadata( + metadata, target.Metadata, + x.Index, this.entries[x.Index], x.Compression, bitsPerPixel, @@ -146,7 +148,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals int bpp = 0; for (int i = 0; i < frames.Length; i++) { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; ReadOnlyMemory? colorTable = null; ref IconDirEntry entry = ref this.entries[i]; @@ -198,7 +200,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals frames[i] = frameMetadata; this.SetFrameMetadata( + metadata, frames[i], + i, this.entries[i], isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, bitsPerPixel, @@ -220,11 +224,13 @@ internal abstract class IconDecoderCore : IImageDecoderInternals metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); } - return new(new(bpp), this.Dimensions, metadata, frames); + return new(this.Dimensions, metadata, frames); } protected abstract void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 2433396612..509c9f420c 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -167,7 +167,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals { this.Compression = compression; this.BmpBitsPerPixel = compression == IconFrameCompression.Png - ? BmpBitsPerPixel.Pixel32 + ? BmpBitsPerPixel.Bit32 : bmpBitsPerPixel; this.ColorTable = colorTable; this.iconDirEntry = iconDirEntry; diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index 5d7f84acfb..73d1145883 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -3,7 +3,9 @@ // using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -121,6 +123,108 @@ public static partial class ImageExtensions encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path) => SaveAsCur(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path) => SaveAsCurAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsCurAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path, CurEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream) + => SaveAsCur(source, stream, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsCurAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream, CurEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Gif format. /// @@ -223,6 +327,108 @@ public static partial class ImageExtensions encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path) => SaveAsIco(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path) => SaveAsIcoAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsIcoAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path, IcoEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream) + => SaveAsIco(source, stream, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsIcoAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream, IcoEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Jpeg format. /// diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index 826f5905b1..e35d00ed39 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -4,7 +4,9 @@ // using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -40,6 +42,26 @@ public static class ImageMetadataExtensions /// The new public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static CurMetadata GetCurMetadata(this ImageMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static CurMetadata CloneCurMetadata(this ImageMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -60,6 +82,26 @@ public static class ImageMetadataExtensions /// The new public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static IcoMetadata GetIcoMetadata(this ImageMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -201,6 +243,46 @@ public static class ImageMetadataExtensions public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude index 24ac66a70b..89940d406e 100644 --- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -5,7 +5,9 @@ <#+ private static readonly string[] formats = new []{ "Bmp", + "Cur", "Gif", + "Ico", "Jpeg", "Pbm", "Png", @@ -16,6 +18,8 @@ }; private static readonly string[] frameFormats = new []{ + "Cur", + "Ico", "Gif", "Png", "Tiff", diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index bcc7484b3b..9c0de1edbe 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -109,7 +109,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable if (this.DecodedImageFormat is not null && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) { - return TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + this.formatMetadata[key] = derivedMeta; + return derivedMeta; } TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); @@ -119,7 +121,7 @@ public sealed class ImageFrameMetadata : IDeepCloneable internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) where TFormatMetadata : class - where TFormatFrameMetadata : class, IDeepCloneable + where TFormatFrameMetadata : class, IFormatFrameMetadata => this.formatMetadata[key] = value; /// diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 700c7fe83c..b5c46d7582 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -194,7 +194,9 @@ public sealed class ImageMetadata : IDeepCloneable if (this.DecodedImageFormat is not null && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) { - return TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + TFormatMetadata derivedMeta = TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + this.formatMetadata[key] = derivedMeta; + return derivedMeta; } // Fall back to a default instance. @@ -217,7 +219,7 @@ public sealed class ImageMetadata : IDeepCloneable => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value) - where TFormatMetadata : class, IDeepCloneable + where TFormatMetadata : class, IFormatMetadata => this.formatMetadata[key] = value; /// diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index 4efd336482..f7ee7614af 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -23,7 +23,7 @@ public class CurDecoderTests Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -36,6 +36,6 @@ public class CurDecoderTests Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index b9b66296df..59c40c9245 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -2,9 +2,11 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; +using static SixLabors.ImageSharp.Tests.TestImages.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; @@ -17,7 +19,7 @@ public class CurEncoderTests [WithFile(CurReal, PixelTypes.Rgba32)] [WithFile(WindowsMouse, PixelTypes.Rgba32)] public void CanRoundTripEncoder(TestImageProvider provider) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(CurDecoder.Instance); using MemoryStream memStream = new(); @@ -31,4 +33,34 @@ public class CurEncoderTests encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); } + + [Theory] + [WithFile(Flutter, PixelTypes.Rgba32)] + public void CanConvertFromIco(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(IcoDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, Encoder); + memStream.Seek(0, SeekOrigin.Begin); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider); + + // Despite preservation of the palette. The process can still be lossy + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); + + for (int i = 0; i < image.Frames.Count; i++) + { + IcoFrameMetadata icoFrame = image.Frames[i].Metadata.GetIcoMetadata(); + CurFrameMetadata curFrame = encoded.Frames[i].Metadata.GetCurMetadata(); + + // Compression may differ as we cannot convert that. + // Color table may differ. + Assert.Equal(icoFrame.BmpBitsPerPixel, curFrame.BmpBitsPerPixel); + Assert.Equal(icoFrame.EncodingWidth, curFrame.EncodingWidth); + Assert.Equal(icoFrame.EncodingHeight, curFrame.EncodingHeight); + } + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index a776a637b8..bc46df0955 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -56,7 +56,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit1, meta.BmpBitsPerPixel); } [Theory] @@ -92,7 +92,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit24, meta.BmpBitsPerPixel); } [Theory] @@ -128,7 +128,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -163,7 +163,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit4, meta.BmpBitsPerPixel); } [Theory] @@ -199,7 +199,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit8, meta.BmpBitsPerPixel); } [Theory] @@ -229,7 +229,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Png, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -262,7 +262,7 @@ public class IcoDecoderTests { ImageFrame frame = image.Frames[i]; IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata(); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } image.DebugSaveMultiFrame(provider); @@ -327,6 +327,6 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index db28f9f703..751db384d7 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using static SixLabors.ImageSharp.Tests.TestImages.Cur; using static SixLabors.ImageSharp.Tests.TestImages.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; @@ -26,9 +28,38 @@ public class IcoEncoderTests memStream.Seek(0, SeekOrigin.Begin); using Image encoded = Image.Load(memStream); - encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); + encoded.DebugSaveMultiFrame(provider); // Despite preservation of the palette. The process can still be lossy encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); } + + [Theory] + [WithFile(WindowsMouse, PixelTypes.Rgba32)] + public void CanConvertFromCur(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(CurDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, Encoder); + memStream.Seek(0, SeekOrigin.Begin); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider); + + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); + + for (int i = 0; i < image.Frames.Count; i++) + { + CurFrameMetadata curFrame = image.Frames[i].Metadata.GetCurMetadata(); + IcoFrameMetadata icoFrame = encoded.Frames[i].Metadata.GetIcoMetadata(); + + // Compression may differ as we cannot convert that. + Assert.Equal(curFrame.BmpBitsPerPixel, icoFrame.BmpBitsPerPixel); + Assert.Equal(curFrame.EncodingWidth, icoFrame.EncodingWidth); + Assert.Equal(curFrame.EncodingHeight, icoFrame.EncodingHeight); + Assert.Equal(curFrame.ColorTable, icoFrame.ColorTable); + } + } } From 79f38c4badd7e444258fd3906f34eb91c903de61 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 20 Jun 2024 19:50:00 +1000 Subject: [PATCH 27/43] Update BmpEncoder.cs --- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 0be243f9a6..e255568047 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Bmp; From cf3faaaadf854f74d09638a0fe34daa9ce7bf2af Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 17:03:55 +1000 Subject: [PATCH 28/43] Remove IImageDecoderInternals redirect --- src/ImageSharp/Advanced/AotCompilerTools.cs | 25 ++-- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 17 +-- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 17 +-- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- .../Formats/IImageDecoderInternals.cs | 50 ------- src/ImageSharp/Formats/ImageDecoder.cs | 14 +- src/ImageSharp/Formats/ImageDecoderCore.cs | 127 ++++++++++++++++++ .../Formats/ImageDecoderUtilities.cs | 81 ----------- .../Formats/Jpeg/JpegDecoderCore.cs | 16 +-- src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 16 +-- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 18 +-- src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 13 +- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 17 +-- .../Decompressors/WebpTiffCompression.cs | 4 +- .../Formats/Tiff/TiffDecoderCore.cs | 15 +-- .../Formats/Webp/WebpDecoderCore.cs | 41 +++--- .../Formats/Jpg/JpegDecoderTests.cs | 4 +- .../Formats/Jpg/Utils/JpegFixture.cs | 14 +- 18 files changed, 222 insertions(+), 269 deletions(-) delete mode 100644 src/ImageSharp/Formats/IImageDecoderInternals.cs create mode 100644 src/ImageSharp/Formats/ImageDecoderCore.cs delete mode 100644 src/ImageSharp/Formats/ImageDecoderUtilities.cs diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index f36f3d09b4..61ec85b47a 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; @@ -195,39 +196,41 @@ internal static class AotCompilerTools => default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext(default, default, default); /// - /// This method pre-seeds the all in the AoT compiler. + /// This method pre-seeds the all core encoders in the AoT compiler. /// /// The pixel format. [Preserve] private static void AotCompileImageEncoderInternals() where TPixel : unmanaged, IPixel { - default(WebpEncoderCore).Encode(default, default, default); default(BmpEncoderCore).Encode(default, default, default); default(GifEncoderCore).Encode(default, default, default); default(JpegEncoderCore).Encode(default, default, default); default(PbmEncoderCore).Encode(default, default, default); default(PngEncoderCore).Encode(default, default, default); + default(QoiEncoderCore).Encode(default, default, default); default(TgaEncoderCore).Encode(default, default, default); default(TiffEncoderCore).Encode(default, default, default); + default(WebpEncoderCore).Encode(default, default, default); } /// - /// This method pre-seeds the all in the AoT compiler. + /// This method pre-seeds the all in the AoT compiler. /// /// The pixel format. [Preserve] private static void AotCompileImageDecoderInternals() where TPixel : unmanaged, IPixel { - default(WebpDecoderCore).Decode(default, default); - default(BmpDecoderCore).Decode(default, default); - default(GifDecoderCore).Decode(default, default); - default(JpegDecoderCore).Decode(default, default); - default(PbmDecoderCore).Decode(default, default); - default(PngDecoderCore).Decode(default, default); - default(TgaDecoderCore).Decode(default, default); - default(TiffDecoderCore).Decode(default, default); + default(BmpDecoderCore).Decode(default, default, default); + default(GifDecoderCore).Decode(default, default, default); + default(JpegDecoderCore).Decode(default, default, default); + default(PbmDecoderCore).Decode(default, default, default); + default(PngDecoderCore).Decode(default, default, default); + default(QoiDecoderCore).Decode(default, default, default); + default(TgaDecoderCore).Decode(default, default, default); + default(TiffDecoderCore).Decode(default, default, default); + default(WebpDecoderCore).Decode(default, default, default); } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 863fed359c..5dc30575d5 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// A useful decoding source example can be found at /// -internal sealed class BmpDecoderCore : IImageDecoderInternals +internal sealed class BmpDecoderCore : ImageDecoderCore { /// /// The default mask for the red part of the color for 16 bit rgb bitmaps. @@ -104,22 +104,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// /// The options. public BmpDecoderCore(BmpDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.rleSkippedPixelHandling = options.RleSkippedPixelHandling; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { Image? image = null; try @@ -205,7 +198,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); @@ -1369,6 +1362,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; + + this.Dimensions = new(this.infoHeader.Width, this.infoHeader.Height); } /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index aecbbbbc71..b1d357f86d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Performs the gif decoding operation. /// -internal sealed class GifDecoderCore : IImageDecoderInternals +internal sealed class GifDecoderCore : ImageDecoderCore { /// /// The temp buffer used to reduce allocations. @@ -94,8 +94,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// /// The decoder options. public GifDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.skipMetadata = options.SkipMetadata; this.maxFrames = options.MaxFrames; @@ -103,14 +103,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; Image? image = null; @@ -181,7 +174,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageFrameMetadata? previousFrame = null; @@ -287,6 +280,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); } + + this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height); } /// diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 1215768e4b..1daa713cbc 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Implements the GIF encoding protocol. /// -internal sealed class GifEncoderCore : IImageEncoderInternals +internal sealed class GifEncoderCore { /// /// Used for allocating memory during processing operations. diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs deleted file mode 100644 index 06fb597640..0000000000 --- a/src/ImageSharp/Formats/IImageDecoderInternals.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Abstraction for shared internals for XXXDecoderCore implementations to be used with . -/// -internal interface IImageDecoderInternals -{ - /// - /// Gets the general decoder options. - /// - DecoderOptions Options { get; } - - /// - /// Gets the dimensions of the image being decoded. - /// - Size Dimensions { get; } - - /// - /// Decodes the image from the specified stream. - /// - /// The pixel format. - /// The stream, where the image should be decoded from. Cannot be null. - /// The token to monitor for cancellation requests. - /// is null. - /// The decoded image. - /// - /// Cancellable synchronous method. In case of cancellation, - /// an shall be thrown which will be handled on the call site. - /// - Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel; - - /// - /// Reads the raw image information from the specified stream. - /// - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The . - /// - /// Cancellable synchronous method. In case of cancellation, - /// an shall be thrown which will be handled on the call site. - /// - ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); -} diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index ebb45d7013..549a28d409 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -189,7 +189,7 @@ public abstract class ImageDecoder : IImageDecoder throw new NotSupportedException("Cannot read from the stream."); } - T PeformActionAndResetPosition(Stream s, long position) + T PerformActionAndResetPosition(Stream s, long position) { T result = action(s); @@ -206,7 +206,7 @@ public abstract class ImageDecoder : IImageDecoder if (stream.CanSeek) { - return PeformActionAndResetPosition(stream, stream.Position); + return PerformActionAndResetPosition(stream, stream.Position); } Configuration configuration = options.Configuration; @@ -231,7 +231,7 @@ public abstract class ImageDecoder : IImageDecoder throw new NotSupportedException("Cannot read from the stream."); } - Task PeformActionAndResetPosition(Stream s, long position, CancellationToken ct) + Task PerformActionAndResetPosition(Stream s, long position, CancellationToken ct) { try { @@ -263,15 +263,15 @@ public abstract class ImageDecoder : IImageDecoder // code below to copy the stream to an in-memory buffer before invoking the action. if (stream is MemoryStream ms) { - return PeformActionAndResetPosition(ms, ms.Position, cancellationToken); + return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } if (stream is ChunkedMemoryStream cms) { - return PeformActionAndResetPosition(cms, cms.Position, cancellationToken); + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); } - return CopyToMemoryStreamAndActionAsync(options, stream, PeformActionAndResetPosition, cancellationToken); + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } private static async Task CopyToMemoryStreamAndActionAsync( @@ -282,7 +282,7 @@ public abstract class ImageDecoder : IImageDecoder { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs new file mode 100644 index 0000000000..adf0107da0 --- /dev/null +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// The base class for all stateful image decoders. +/// +internal abstract class ImageDecoderCore +{ + /// + /// Initializes a new instance of the class. + /// + /// The general decoder options. + protected ImageDecoderCore(DecoderOptions options) + => this.Options = options; + + /// + /// Gets the general decoder options. + /// + public DecoderOptions Options { get; } + + /// + /// Gets or sets the dimensions of the image being decoded. + /// + public Size Dimensions { get; protected internal set; } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public ImageInfo Identify( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + { + using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); + + try + { + return this.Identify(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + } + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. + BufferedReadStream bufferedReadStream = + stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); + + try + { + return this.Decode(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + protected abstract ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The token to monitor for cancellation requests. + /// is null. + /// The decoded image. + /// + /// Cancellable synchronous method. In case of cancellation, an shall + /// be thrown which will be handled on the call site. + /// + protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; +} diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs deleted file mode 100644 index a1abd7dc30..0000000000 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Utility methods for . -/// -internal static class ImageDecoderUtilities -{ - internal static ImageInfo Identify( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - { - using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); - - try - { - return decoder.Identify(bufferedReadStream, cancellationToken); - } - catch (InvalidMemoryOperationException ex) - { - throw new InvalidImageContentException(decoder.Dimensions, ex); - } - catch (Exception) - { - throw; - } - } - - internal static Image Decode( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); - - internal static Image Decode( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - Func largeImageExceptionFactory, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. - BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); - - try - { - return decoder.Decode(bufferedReadStream, cancellationToken); - } - catch (InvalidMemoryOperationException ex) - { - throw largeImageExceptionFactory(ex, decoder.Dimensions); - } - catch (Exception) - { - throw; - } - finally - { - if (bufferedReadStream != stream) - { - bufferedReadStream.Dispose(); - } - } - } - - private static InvalidImageContentException DefaultLargeImageExceptionFactory( - InvalidMemoryOperationException memoryOperationException, - Size dimensions) => - new(dimensions, memoryOperationException); -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index ccace190f9..b0bc66008a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// Originally ported from /// with additional fixes for both performance and common encoding errors. /// -internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals +internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { /// /// Whether the image has an EXIF marker. @@ -117,8 +117,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// /// The decoder options. public JpegDecoderCore(JpegDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.resizeMode = options.ResizeMode; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -130,12 +130,6 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals // Refers to assembly's static data segment, no allocation occurs. private static ReadOnlySpan SupportedPrecisions => new byte[] { 8, 12 }; - /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => this.Frame.PixelSize; - /// /// Gets the frame /// @@ -198,8 +192,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { using SpectralConverter spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); this.ParseStream(stream, spectralConverter, cancellationToken); @@ -216,7 +209,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ParseStream(stream, spectralConverter: null, cancellationToken); this.InitExifProfile(); @@ -1216,6 +1209,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); + this.Dimensions = new(frameWidth, frameHeight); this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; remaining -= length; diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 3fe339865b..73a5085c98 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Performs the PBM decoding operation. /// -internal sealed class PbmDecoderCore : IImageDecoderInternals +internal sealed class PbmDecoderCore : ImageDecoderCore { private int maxPixelValue; @@ -52,20 +52,13 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals /// /// The decoder options. public PbmDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => this.pixelSize; - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); @@ -83,7 +76,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); @@ -179,6 +172,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals } this.pixelSize = new Size(width, height); + this.Dimensions = this.pixelSize; this.metadata = new ImageMetadata(); PbmMetadata meta = this.metadata.GetPbmMetadata(); meta.Encoding = this.encoding; diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index dd5e16d7be..1eeac42b83 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Performs the png decoding operation. /// -internal sealed class PngDecoderCore : IImageDecoderInternals +internal sealed class PngDecoderCore : ImageDecoderCore { /// /// The general decoder options. @@ -130,8 +130,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The decoder options. public PngDecoderCore(PngDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.configuration = options.GeneralOptions.Configuration; this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -141,8 +141,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.colorMetadataOnly = colorMetadataOnly; this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = true; @@ -153,14 +153,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.header.Width, this.header.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageMetadata metadata = new(); @@ -335,7 +328,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageMetadata metadata = new(); @@ -1339,6 +1332,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals pngMetadata.InterlaceMethod = this.header.InterlaceMethod; this.pngColorType = this.header.ColorType; + this.Dimensions = new(this.header.Width, this.header.Height); } /// diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index deb0a37f05..8552e164d8 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -13,7 +13,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Qoi; -internal class QoiDecoderCore : IImageDecoderInternals +internal class QoiDecoderCore : ImageDecoderCore { /// /// The global configuration. @@ -31,19 +31,14 @@ internal class QoiDecoderCore : IImageDecoderInternals private QoiHeader header; public QoiDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } - public DecoderOptions Options { get; } - - public Size Dimensions { get; } - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { // Process the header to get metadata this.ProcessHeader(stream); @@ -68,7 +63,7 @@ internal class QoiDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); PixelTypeInfo pixelType = new(8 * (int)this.header.Channels); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 34f4c2bcf1..e2dd919d29 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// /// Performs the tga decoding operation. /// -internal sealed class TgaDecoderCore : IImageDecoderInternals +internal sealed class TgaDecoderCore : ImageDecoderCore { /// /// General configuration options. @@ -52,21 +52,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals /// /// The options. public TgaDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.fileHeader.Width, this.fileHeader.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { try { @@ -653,7 +646,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadFileHeader(stream); return new ImageInfo( @@ -933,6 +926,8 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals stream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); + this.Dimensions = new Size(this.fileHeader.Width, this.fileHeader.Height); + this.metadata = new ImageMetadata(); this.tgaMetadata = this.metadata.GetTgaMetadata(); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index 416472e830..76d0bb6418 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -32,8 +32,8 @@ internal class WebpTiffCompression : TiffBaseDecompressor /// protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { - using WebpDecoderCore decoder = new(new WebpDecoderOptions()); - using Image image = decoder.Decode(stream, cancellationToken); + using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = this.options }); + using Image image = decoder.Decode(this.options.Configuration, stream, cancellationToken); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index aed6d4ec66..96e6d2a89d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Performs the tiff decoding operation. /// -internal class TiffDecoderCore : IImageDecoderInternals +internal class TiffDecoderCore : ImageDecoderCore { /// /// General configuration options. @@ -60,8 +60,8 @@ internal class TiffDecoderCore : IImageDecoderInternals /// /// The decoder options. public TiffDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.skipMetadata = options.SkipMetadata; this.maxFrames = options.MaxFrames; @@ -154,14 +154,7 @@ internal class TiffDecoderCore : IImageDecoderInternals public TiffPredictor Predictor { get; set; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions { get; private set; } - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { List> frames = new(); List framesMetadata = new(); @@ -215,7 +208,7 @@ internal class TiffDecoderCore : IImageDecoderInternals } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 21a25860c7..781b8246ff 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Performs the webp decoding operation. /// -internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable +internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable { /// /// General configuration options. @@ -61,8 +61,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// /// The decoder options. public WebpDecoderCore(WebpDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.backgroundColorHandling = options.BackgroundColorHandling; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -70,15 +70,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable this.memoryAllocator = this.configuration.MemoryAllocator; } - /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { Image? image = null; try @@ -136,7 +129,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { ReadImageHeader(stream, stackalloc byte[4]); @@ -186,36 +179,39 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + WebpImageInfo webpImageInfo; WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: webpMetadata.FileFormat = WebpFileFormatType.Lossy; - return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + break; case WebpChunkType.Vp8L: webpMetadata.FileFormat = WebpFileFormatType.Lossless; - return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + break; case WebpChunkType.Vp8X: - WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); while (stream.Position < stream.Length) { chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Vp8) { webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); } else if (chunkType == WebpChunkType.Vp8L) { webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { - return webpInfos; + break; } } else @@ -226,12 +222,17 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable } } - return webpInfos; + break; default: WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); - return - new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + + // This return will never be reached, because throw helper will throw an exception. + webpImageInfo = new(); + break; } + + this.Dimensions = new Size((int)webpImageInfo.Width, (int)webpImageInfo.Height); + return webpImageInfo; } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 2fe4282607..97be5d8383 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -67,11 +67,11 @@ public partial class JpegDecoderTests public void ParseStream_BasicPropertiesAreCorrect() { JpegDecoderOptions options = new(); + Configuration configuration = options.GeneralOptions.Configuration; byte[] bytes = TestFile.Create(TestImages.Jpeg.Progressive.Progress).Bytes; using MemoryStream ms = new(bytes); - using BufferedReadStream bufferedStream = new(Configuration.Default, ms); using JpegDecoderCore decoder = new(options); - using Image image = decoder.Decode(bufferedStream, cancellationToken: default); + using Image image = decoder.Decode(configuration, ms, cancellationToken: default); // I don't know why these numbers are different. All I know is that the decoder works // and spectral data is exactly correct also. diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 978978989e..a3fbe4018e 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Text; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -216,18 +215,17 @@ public class JpegFixture : MeasureFixture internal static JpegDecoderCore ParseJpegStream(string testFileName, bool metaDataOnly = false) { byte[] bytes = TestFile.Create(testFileName).Bytes; - using var ms = new MemoryStream(bytes); - using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); - - JpegDecoderOptions options = new(); - var decoder = new JpegDecoderCore(options); + using MemoryStream ms = new(bytes); + JpegDecoderOptions decoderOptions = new(); + Configuration configuration = decoderOptions.GeneralOptions.Configuration; + JpegDecoderCore decoder = new(decoderOptions); if (metaDataOnly) { - decoder.Identify(bufferedStream, cancellationToken: default); + decoder.Identify(configuration, ms, default); } else { - using Image image = decoder.Decode(bufferedStream, cancellationToken: default); + using Image image = decoder.Decode(configuration, ms, default); } return decoder; From e61585afe28c8a0b1fdba86662159c842c5e8517 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 17:04:18 +1000 Subject: [PATCH 29/43] Remove IImageEncoderInternals interface --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 3 +-- .../Formats/IImageEncoderInternals.cs | 22 ------------------- src/ImageSharp/Formats/ImageEncoder.cs | 1 - .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs | 3 +-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs | 10 +++++++-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 3 +-- .../Formats/Tiff/TiffEncoderCore.cs | 3 +-- .../Formats/Webp/WebpEncoderCore.cs | 2 +- 10 files changed, 15 insertions(+), 36 deletions(-) delete mode 100644 src/ImageSharp/Formats/IImageEncoderInternals.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 076d1adf00..187170f898 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -17,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// Image encoder for writing an image to a stream as a Windows bitmap. /// -internal sealed class BmpEncoderCore : IImageEncoderInternals +internal sealed class BmpEncoderCore { /// /// The amount to pad each row by. diff --git a/src/ImageSharp/Formats/IImageEncoderInternals.cs b/src/ImageSharp/Formats/IImageEncoderInternals.cs deleted file mode 100644 index 131949c968..0000000000 --- a/src/ImageSharp/Formats/IImageEncoderInternals.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Abstraction for shared internals for ***DecoderCore implementations. -/// -internal interface IImageEncoderInternals -{ - /// - /// Encodes the image. - /// - /// The image. - /// The stream. - /// The token to monitor for cancellation requests. - /// The pixel type. - void Encode(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel; -} diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index 4acd29e81c..deb527f698 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 95f7fde32c..4477df35cd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// /// Image encoder for writing an image to a stream as a jpeg. /// -internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals +internal sealed unsafe partial class JpegEncoderCore { /// /// The available encodable frame configs. diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index b6e31a3c28..843f1880e6 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Text; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm; @@ -10,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Image encoder for writing an image to a stream as a PGM, PBM, PPM or PAM bitmap. /// -internal sealed class PbmEncoderCore : IImageEncoderInternals +internal sealed class PbmEncoderCore { private const byte NewLine = (byte)'\n'; private const byte Space = (byte)' '; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 802e4dd6a4..794b306fc1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Performs the png encoding operation. /// -internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable +internal sealed class PngEncoderCore : IDisposable { /// /// The maximum block size, defaults at 64k for uncompressed blocks. diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs index 53f67e765d..88d87a3825 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi; /// /// Image encoder for writing an image to a stream as a QOi image /// -internal class QoiEncoderCore : IImageEncoderInternals +internal class QoiEncoderCore { /// /// The encoder with options @@ -41,7 +41,13 @@ internal class QoiEncoderCore : IImageEncoderInternals this.memoryAllocator = configuration.MemoryAllocator; } - /// + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index bbb476c017..c1c3d23b1f 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -15,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// /// Image encoder for writing an image to a stream as a truevision targa image. /// -internal sealed class TgaEncoderCore : IImageEncoderInternals +internal sealed class TgaEncoderCore { /// /// Used for allocating memory during processing operations. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 149f23f1bf..59b4eac0bc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. #nullable disable -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Constants; @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Performs the TIFF encoding operation. /// -internal sealed class TiffEncoderCore : IImageEncoderInternals +internal sealed class TiffEncoderCore { private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian ? TiffConstants.ByteOrderLittleEndianShort diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d29759f9a1..5d904380bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image encoder for writing an image to a stream in the Webp format. /// -internal sealed class WebpEncoderCore : IImageEncoderInternals +internal sealed class WebpEncoderCore { /// /// Used for allocating memory during processing operations. From dc6871cfc350da37b8f47acc2a680443392939b7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 22:02:23 +1000 Subject: [PATCH 30/43] Add explicit AOT seeding for SpectralConverter --- src/ImageSharp/Advanced/AotCompilerTools.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 61ec85b47a..0d5faabe18 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -10,11 +10,13 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -130,6 +132,7 @@ internal static class AotCompilerTools AotCompileImageDecoderInternals(); AotCompileImageEncoders(); AotCompileImageDecoders(); + AotCompileSpectralConverter(); AotCompileImageProcessors(); AotCompileGenericImageProcessors(); AotCompileResamplers(); @@ -269,6 +272,17 @@ internal static class AotCompilerTools AotCompileImageDecoder(); } + [Preserve] + private static void AotCompileSpectralConverter() + where TPixel : unmanaged, IPixel + { + default(SpectralConverter).GetPixelBuffer(default); + default(GrayJpegSpectralConverter).GetPixelBuffer(default); + default(RgbJpegSpectralConverter).GetPixelBuffer(default); + default(TiffJpegSpectralConverter).GetPixelBuffer(default); + default(TiffOldJpegSpectralConverter).GetPixelBuffer(default); + } + /// /// This method pre-seeds the in the AoT compiler. /// From 9bbc70e5e3c9ff57918141496ff3eddee722e16b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 25 Jul 2024 17:08:53 +1000 Subject: [PATCH 31/43] Cleanup --- src/ImageSharp/Formats/Icon/IconDecoderCore.cs | 10 +++++----- src/ImageSharp/ImageInfo.cs | 9 +++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index 0feed7f693..ce72f15612 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -170,7 +170,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); // 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); + ImageInfo frameInfo = this.GetDecoder(isPng).Identify(stream, cancellationToken); ImageFrameMetadata frameMetadata = new(); @@ -178,14 +178,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals { if (i == 0) { - pngMetadata = temp.Metadata.GetPngMetadata(); + pngMetadata = frameInfo.Metadata.GetPngMetadata(); } - frameMetadata.SetFormatMetadata(PngFormat.Instance, temp.FrameMetadataCollection[0].GetPngMetadata()); + frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata()); } else { - BmpMetadata meta = temp.Metadata.GetBmpMetadata(); + BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata(); bitsPerPixel = meta.BitsPerPixel; colorTable = meta.ColorTable; @@ -210,7 +210,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals // 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)); + this.Dimensions = new(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height)); } // Copy the format specific metadata to the image. diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index 3a2e870176..0bbd73b63a 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -62,6 +62,11 @@ public class ImageInfo /// public int Height => this.Size.Height; + /// + /// Gets the number of frames in the image. + /// + public int FrameCount => this.FrameMetadataCollection.Count; + /// /// Gets any metadata associated with the image. /// @@ -75,10 +80,10 @@ public class ImageInfo /// /// Gets the size of the image in px units. /// - public Size Size { get; internal set; } + public Size Size { get; } /// /// Gets the bounds of the image. /// - public Rectangle Bounds => new(0, 0, this.Width, this.Height); + public Rectangle Bounds => new(Point.Empty, this.Size); } From 934a24ad134e5891d1633b92a0f1c2a02c3a4ff7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 31 Jul 2024 22:53:00 +1000 Subject: [PATCH 32/43] Migrate icon codec --- .../Formats/Icon/IconDecoderCore.cs | 24 +++++++++---------- .../Formats/Icon/IconEncoderCore.cs | 4 +++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index ce72f15612..caed2dd902 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -6,24 +6,21 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Icon; -internal abstract class IconDecoderCore : IImageDecoderInternals +internal abstract class IconDecoderCore : ImageDecoderCore { private IconDir fileHeader; private IconDirEntry[]? entries; protected IconDecoderCore(DecoderOptions options) - => this.Options = options; - - public DecoderOptions Options { get; } - - public Size Dimensions { get; private set; } + : base(options) + { + } - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { // Stream may not at 0. long basePosition = stream.Position; @@ -61,7 +58,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. - Image temp = this.GetDecoder(isPng).Decode(stream, cancellationToken); + Image temp = this.GetDecoder(isPng).Decode(this.Options.Configuration, stream, cancellationToken); decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i)); // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data @@ -133,7 +130,8 @@ internal abstract class IconDecoderCore : IImageDecoderInternals return result; } - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { // Stream may not at 0. long basePosition = stream.Position; @@ -170,7 +168,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. - ImageInfo frameInfo = this.GetDecoder(isPng).Identify(stream, cancellationToken); + ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken); ImageFrameMetadata frameMetadata = new(); @@ -281,7 +279,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals this.Dimensions = new(width, height); } - private IImageDecoderInternals GetDecoder(bool isPng) + private ImageDecoderCore GetDecoder(bool isPng) { if (isPng) { diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 509c9f420c..4b973d5115 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -11,7 +11,7 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Icon; -internal abstract class IconEncoderCore : IImageEncoderInternals +internal abstract class IconEncoderCore { private readonly QuantizingImageEncoder encoder; private readonly IconFileType iconFileType; @@ -43,6 +43,8 @@ internal abstract class IconEncoderCore : IImageEncoderInternals for (int i = 0; i < image.Frames.Count; i++) { + cancellationToken.ThrowIfCancellationRequested(); + // 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]; From 1102eb54e97ee137d19e8b0fa796f213b36f0d4c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 1 Aug 2024 11:28:00 +1000 Subject: [PATCH 33/43] Replace PngCrcChunkHandling --- src/ImageSharp/Formats/DecoderOptions.cs | 5 ++++ src/ImageSharp/Formats/Png/PngChunk.cs | 10 +++---- .../Formats/Png/PngCrcChunkHandling.cs | 30 ------------------- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 12 ++++---- .../Formats/Png/PngDecoderOptions.cs | 5 ---- .../Formats/SegmentErrorHandling.cs | 30 +++++++++++++++++++ .../Formats/Png/PngDecoderTests.cs | 18 +++++++++-- 7 files changed, 62 insertions(+), 48 deletions(-) delete mode 100644 src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs create mode 100644 src/ImageSharp/Formats/SegmentErrorHandling.cs diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index 6243a071d5..be906d7d93 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -55,5 +55,10 @@ public sealed class DecoderOptions /// public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); } + /// + /// Gets the segment error handling strategy to use during decoding. + /// + public SegmentErrorHandling SegmentErrorHandling { get; init; } = SegmentErrorHandling.IgnoreNonCritical; + internal void SetConfiguration(Configuration configuration) => this.configuration = configuration; } diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index 6ec0df9ad6..666f51daad 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -41,13 +41,13 @@ internal readonly struct PngChunk /// /// Gets a value indicating whether the given chunk is critical to decoding /// - /// The chunk CRC handling behavior. - public bool IsCritical(PngCrcChunkHandling handling) + /// The segment handling behavior. + public bool IsCritical(SegmentErrorHandling handling) => handling switch { - PngCrcChunkHandling.IgnoreNone => true, - PngCrcChunkHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, - PngCrcChunkHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, + SegmentErrorHandling.IgnoreNone => true, + SegmentErrorHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, + SegmentErrorHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, _ => false, }; } diff --git a/src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs b/src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs deleted file mode 100644 index 264d737fdc..0000000000 --- a/src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Png; - -/// -/// Specifies how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG. -/// -public enum PngCrcChunkHandling -{ - /// - /// Do not ignore any CRC chunk errors. - /// - IgnoreNone, - - /// - /// Ignore CRC errors in non critical chunks. - /// - IgnoreNonCritical, - - /// - /// Ignore CRC errors in data chunks. - /// - IgnoreData, - - /// - /// Ignore CRC errors in all chunks. - /// - IgnoreAll -} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 01f038141a..080c35c0d1 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -119,7 +119,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore /// /// How to handle CRC errors. /// - private readonly PngCrcChunkHandling pngCrcChunkHandling; + private readonly SegmentErrorHandling segmentErrorHandling; /// /// A reusable Crc32 hashing instance. @@ -142,7 +142,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = options.GeneralOptions.SkipMetadata; this.memoryAllocator = this.configuration.MemoryAllocator; - this.pngCrcChunkHandling = options.PngCrcChunkHandling; + this.segmentErrorHandling = options.GeneralOptions.SegmentErrorHandling; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; } @@ -154,7 +154,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.skipMetadata = true; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; - this.pngCrcChunkHandling = options.PngCrcChunkHandling; + this.segmentErrorHandling = options.GeneralOptions.SegmentErrorHandling; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; } @@ -833,7 +833,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll) + if (this.segmentErrorHandling is SegmentErrorHandling.IgnoreData or SegmentErrorHandling.IgnoreAll) { goto EXIT; } @@ -939,7 +939,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll) + if (this.segmentErrorHandling is SegmentErrorHandling.IgnoreData or SegmentErrorHandling.IgnoreAll) { goto EXIT; } @@ -1927,7 +1927,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore private void ValidateChunk(in PngChunk chunk, Span buffer) { uint inputCrc = this.ReadChunkCrc(buffer); - if (chunk.IsCritical(this.pngCrcChunkHandling)) + if (chunk.IsCritical(this.segmentErrorHandling)) { Span chunkType = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); diff --git a/src/ImageSharp/Formats/Png/PngDecoderOptions.cs b/src/ImageSharp/Formats/Png/PngDecoderOptions.cs index abfa4b1da8..a73db87774 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderOptions.cs @@ -11,11 +11,6 @@ public sealed class PngDecoderOptions : ISpecializedDecoderOptions /// public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); - /// - /// Gets a value indicating how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG. - /// - public PngCrcChunkHandling PngCrcChunkHandling { get; init; } = PngCrcChunkHandling.IgnoreNonCritical; - /// /// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed. /// Defaults to 8MB diff --git a/src/ImageSharp/Formats/SegmentErrorHandling.cs b/src/ImageSharp/Formats/SegmentErrorHandling.cs new file mode 100644 index 0000000000..7b28de5895 --- /dev/null +++ b/src/ImageSharp/Formats/SegmentErrorHandling.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Specifies how to handle validation of errors in different segments of encoded image files. +/// +public enum SegmentErrorHandling +{ + /// + /// Do not ignore any errors. + /// + IgnoreNone, + + /// + /// Ignore errors in non-critical segments of the encoded image. + /// + IgnoreNonCritical, + + /// + /// Ignore errors in data segments (e.g., image data, metadata). + /// + IgnoreData, + + /// + /// Ignore errors in all segments. + /// + IgnoreAll +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 8492d78f86..c3a6b11f5a 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -381,6 +381,20 @@ public partial class PngDecoderTests Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); } + [Theory] + [InlineData(TestImages.Png.Bad.WrongCrcDataChunk, 1)] + [InlineData(TestImages.Png.Bad.Issue2589, 24)] + public void Identify_IgnoreCrcErrors(string imagePath, int expectedPixelSize) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + ImageInfo imageInfo = Image.Identify(new DecoderOptions() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData }, stream); + + Assert.NotNull(imageInfo); + Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); + } + [Theory] [WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)] public void Decode_MissingDataChunk_ThrowsException(TestImageProvider provider) @@ -479,7 +493,7 @@ public partial class PngDecoderTests public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors(TestImageProvider provider, bool compare) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(PngDecoder.Instance, new PngDecoderOptions() { PngCrcChunkHandling = PngCrcChunkHandling.IgnoreData }); + using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData }); image.DebugSave(provider); if (compare) @@ -660,7 +674,7 @@ public partial class PngDecoderTests public void Binary_PrematureEof() { PngDecoder decoder = PngDecoder.Instance; - PngDecoderOptions options = new() { PngCrcChunkHandling = PngCrcChunkHandling.IgnoreData }; + PngDecoderOptions options = new() { GeneralOptions = new() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData } }; using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options); // TODO: Try to reduce this to 1. From 4a6b51bc288f13978adcc59f618615216c5db956 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 1 Aug 2024 15:25:41 +1000 Subject: [PATCH 34/43] Rename --- src/ImageSharp/Formats/DecoderOptions.cs | 2 +- src/ImageSharp/Formats/Png/PngChunk.cs | 8 ++++---- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 12 ++++++------ ...tErrorHandling.cs => SegmentIntegrityHandling.cs} | 2 +- .../ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) rename src/ImageSharp/Formats/{SegmentErrorHandling.cs => SegmentIntegrityHandling.cs} (94%) diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index be906d7d93..3b16159b7e 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -58,7 +58,7 @@ public sealed class DecoderOptions /// /// Gets the segment error handling strategy to use during decoding. /// - public SegmentErrorHandling SegmentErrorHandling { get; init; } = SegmentErrorHandling.IgnoreNonCritical; + public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical; internal void SetConfiguration(Configuration configuration) => this.configuration = configuration; } diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index 666f51daad..3883986d06 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -42,12 +42,12 @@ internal readonly struct PngChunk /// Gets a value indicating whether the given chunk is critical to decoding /// /// The segment handling behavior. - public bool IsCritical(SegmentErrorHandling handling) + public bool IsCritical(SegmentIntegrityHandling handling) => handling switch { - SegmentErrorHandling.IgnoreNone => true, - SegmentErrorHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, - SegmentErrorHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, + SegmentIntegrityHandling.IgnoreNone => true, + SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, + SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, _ => false, }; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 080c35c0d1..484241d52f 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -119,7 +119,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore /// /// How to handle CRC errors. /// - private readonly SegmentErrorHandling segmentErrorHandling; + private readonly SegmentIntegrityHandling segmentIntegrityHandling; /// /// A reusable Crc32 hashing instance. @@ -142,7 +142,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = options.GeneralOptions.SkipMetadata; this.memoryAllocator = this.configuration.MemoryAllocator; - this.segmentErrorHandling = options.GeneralOptions.SegmentErrorHandling; + this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; } @@ -154,7 +154,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.skipMetadata = true; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; - this.segmentErrorHandling = options.GeneralOptions.SegmentErrorHandling; + this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; } @@ -833,7 +833,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.segmentErrorHandling is SegmentErrorHandling.IgnoreData or SegmentErrorHandling.IgnoreAll) + if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll) { goto EXIT; } @@ -939,7 +939,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.segmentErrorHandling is SegmentErrorHandling.IgnoreData or SegmentErrorHandling.IgnoreAll) + if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll) { goto EXIT; } @@ -1927,7 +1927,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore private void ValidateChunk(in PngChunk chunk, Span buffer) { uint inputCrc = this.ReadChunkCrc(buffer); - if (chunk.IsCritical(this.segmentErrorHandling)) + if (chunk.IsCritical(this.segmentIntegrityHandling)) { Span chunkType = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); diff --git a/src/ImageSharp/Formats/SegmentErrorHandling.cs b/src/ImageSharp/Formats/SegmentIntegrityHandling.cs similarity index 94% rename from src/ImageSharp/Formats/SegmentErrorHandling.cs rename to src/ImageSharp/Formats/SegmentIntegrityHandling.cs index 7b28de5895..977aee4ad5 100644 --- a/src/ImageSharp/Formats/SegmentErrorHandling.cs +++ b/src/ImageSharp/Formats/SegmentIntegrityHandling.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats; /// /// Specifies how to handle validation of errors in different segments of encoded image files. /// -public enum SegmentErrorHandling +public enum SegmentIntegrityHandling { /// /// Do not ignore any errors. diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index c3a6b11f5a..9f3c5f6828 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -389,7 +389,7 @@ public partial class PngDecoderTests TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(new DecoderOptions() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData }, stream); + ImageInfo imageInfo = Image.Identify(new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }, stream); Assert.NotNull(imageInfo); Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); @@ -493,7 +493,7 @@ public partial class PngDecoderTests public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors(TestImageProvider provider, bool compare) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData }); + using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }); image.DebugSave(provider); if (compare) @@ -674,7 +674,7 @@ public partial class PngDecoderTests public void Binary_PrematureEof() { PngDecoder decoder = PngDecoder.Instance; - PngDecoderOptions options = new() { GeneralOptions = new() { SegmentErrorHandling = SegmentErrorHandling.IgnoreData } }; + PngDecoderOptions options = new() { GeneralOptions = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData } }; using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options); // TODO: Try to reduce this to 1. From 5b5e599fb95bd763846d0df387d5407229062736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lasse=20V=C3=A5gs=C3=A6ther=20Karlsen?= Date: Wed, 31 Jul 2024 15:17:24 +0200 Subject: [PATCH 35/43] Add DebuggerDisplayAttribute to ExifValue --- src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs index eacb41cfb3..41b947e20c 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs @@ -1,10 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; +[DebuggerDisplay("{Tag} = {IsArray?\"[..]\":ToString(),nq} ({GetType().Name,nq})")] internal abstract class ExifValue : IExifValue, IEquatable { protected ExifValue(ExifTag tag) => this.Tag = tag; From 1e4b895e3da611a434fc2ade00b0284f7314d3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lasse=20V=C3=A5gs=C3=A6ther=20Karlsen?= Date: Wed, 31 Jul 2024 15:22:30 +0200 Subject: [PATCH 36/43] Add DebuggerDisplayAttribute to IptcValue --- src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs index 1a75ecba22..78f7f6de08 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics; using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc; @@ -8,6 +9,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc; /// /// Represents a single value of the IPTC profile. /// +[DebuggerDisplay("{Tag} = {ToString(),nq} ({GetType().Name,nq})")] public sealed class IptcValue : IDeepCloneable { private byte[] data = Array.Empty(); From eb5c05efe1e107d5a560c6f8ed48126f53d50499 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 1 Aug 2024 21:46:58 +1000 Subject: [PATCH 37/43] Allow decoding Tiff of different frame size. --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Formats/Tiff/TiffDecoderCore.cs | 103 ++++++++++++------ .../Formats/Webp/WebpEncoderCore.cs | 4 +- src/ImageSharp/Image.cs | 2 +- src/ImageSharp/ImageFrame.cs | 20 ++-- .../ImageFrameCollection{TPixel}.cs | 2 +- src/ImageSharp/ImageFrame{TPixel}.cs | 2 +- src/ImageSharp/Image{TPixel}.cs | 4 +- .../Convolution/BokehBlurProcessor{TPixel}.cs | 4 +- .../Convolution2PassProcessor{TPixel}.cs | 2 +- .../ConvolutionProcessor{TPixel}.cs | 2 +- .../Effects/OilPaintingProcessor{TPixel}.cs | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 11 +- .../Tiff/TiffEncoderMultiframeTests.cs | 1 - .../ImageComparison/ExactImageComparer.cs | 3 +- .../ImageComparison/TolerantImageComparer.cs | 3 +- 17 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 11185d90b0..2e05ef782f 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -209,7 +209,7 @@ internal sealed class GifEncoderCore ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 4bbb68358f..978b9184e9 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -231,7 +231,7 @@ internal sealed class PngEncoderCore : IDisposable ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 2356d45e47..0ba9755b15 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore this.byteOrder = reader.ByteOrder; this.isBigTiff = reader.IsBigTiff; + Size? size = null; uint frameCount = 0; foreach (ExifProfile ifd in directories) { cancellationToken.ThrowIfCancellationRequested(); - ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); + ImageFrame frame = this.DecodeFrame(ifd, size, cancellationToken); + + if (!size.HasValue) + { + size = frame.Size; + } + frames.Add(frame); framesMetadata.Add(frame.Metadata); @@ -181,19 +188,8 @@ internal class TiffDecoderCore : ImageDecoderCore } } + this.Dimensions = frames[0].Size; ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); - - // TODO: Tiff frames can have different sizes. - ImageFrame root = frames[0]; - this.Dimensions = root.Size(); - foreach (ImageFrame frame in frames) - { - if (frame.Size() != root.Size()) - { - TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); - } - } - return new Image(this.configuration, metadata, frames); } catch @@ -235,9 +231,10 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The IFD tags. + /// The previously determined root frame size if decoded. /// The token to monitor cancellation. /// The tiff frame. - private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken) + private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); @@ -245,15 +242,29 @@ internal class TiffDecoderCore : ImageDecoderCore int width = GetImageWidth(tags); int height = GetImageHeight(tags); - ImageFrame frame = new(this.configuration, width, height, imageFrameMetaData); + + // If size has a value and the width/height off the tiff is smaller we much capture the delta. + if (size.HasValue) + { + if (size.Value.Width < width || size.Value.Height < height) + { + TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported."); + } + } + else + { + size = new Size(width, height); + } + + ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, imageFrameMetaData); if (isTiled) { - this.DecodeImageWithTiles(tags, frame, cancellationToken); + this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken); } else { - this.DecodeImageWithStrips(tags, frame, cancellationToken); + this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); } return frame; @@ -278,8 +289,10 @@ internal class TiffDecoderCore : ImageDecoderCore /// The pixel format. /// The IFD tags. /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The token to monitor cancellation. - private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int rowsPerStrip; @@ -302,6 +315,8 @@ internal class TiffDecoderCore : ImageDecoderCore { this.DecodeStripsPlanar( frame, + width, + height, rowsPerStrip, stripOffsets, stripByteCounts, @@ -311,6 +326,8 @@ internal class TiffDecoderCore : ImageDecoderCore { this.DecodeStripsChunky( frame, + width, + height, rowsPerStrip, stripOffsets, stripByteCounts, @@ -324,13 +341,13 @@ internal class TiffDecoderCore : ImageDecoderCore /// The pixel format. /// The IFD tags. /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The token to monitor cancellation. - private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Buffer2D pixels = frame.PixelBuffer; - int width = pixels.Width; - int height = pixels.Height; if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue valueWidth)) { @@ -384,11 +401,20 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The number of rows per strip of data. /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The token to monitor cancellation. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsPlanar( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Channels; @@ -403,18 +429,18 @@ internal class TiffDecoderCore : ImageDecoderCore { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); + int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel); TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); for (int i = 0; i < stripsPerPlane; i++) { cancellationToken.ThrowIfCancellationRequested(); - int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip; int stripIndex = i; for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) @@ -430,7 +456,7 @@ internal class TiffDecoderCore : ImageDecoderCore stripIndex += stripsPerPlane; } - colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); + colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight); } } finally @@ -447,39 +473,48 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The rows per strip. /// The strip offsets. /// The strip byte counts. /// The token to monitor cancellation. - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsChunky( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) { - rowsPerStrip = frame.Height; + rowsPerStrip = height; } - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); + int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); int bitsPerPixel = this.BitsPerPixel; using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { cancellationToken.ThrowIfCancellationRequested(); - int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 ? rowsPerStrip - : frame.Height % rowsPerStrip; + : height % rowsPerStrip; int top = rowsPerStrip * stripIndex; - if (top + stripHeight > frame.Height) + if (top + stripHeight > height) { // Make sure we ignore any strips that are not needed for the image (if too many are present). break; @@ -493,7 +528,7 @@ internal class TiffDecoderCore : ImageDecoderCore stripBufferSpan, cancellationToken); - colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); + colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index e37462fda4..733801d636 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -160,7 +160,7 @@ internal sealed class WebpEncoderCore // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { @@ -235,7 +235,7 @@ internal sealed class WebpEncoderCore // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index d4f773abe1..9a5bd4cf06 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -77,7 +77,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// /// Gets the size of the image in px units. /// - public Size Size { get; internal set; } + public Size Size { get; private set; } /// /// Gets the bounds of the image. diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 2558e1a13a..3c5338fac3 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -25,20 +25,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) { this.Configuration = configuration; - this.Width = width; - this.Height = height; + this.Size = new(width, height); this.Metadata = metadata; } /// - /// Gets the width. + /// Gets the frame width in px units. /// - public int Width { get; private set; } + public int Width => this.Size.Width; /// - /// Gets the height. + /// Gets the frame height in px units. /// - public int Height { get; private set; } + public int Height => this.Size.Height; /// /// Gets the metadata of the frame. @@ -51,8 +50,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Gets the size of the frame. /// - /// The - public Size Size() => new(this.Width, this.Height); + public Size Size { get; private set; } /// /// Gets the bounds of the frame. @@ -80,9 +78,5 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// Updates the size of the image frame. /// /// The size. - internal void UpdateSize(Size size) - { - this.Width = size.Width; - this.Height = size.Height; - } + protected void UpdateSize(Size size) => this.Size = size; } diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index e927fb0fac..ad7d719744 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -414,7 +414,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer { ImageFrame result = new( this.parent.Configuration, - source.Size(), + source.Size, source.Metadata.DeepClone()); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); return result; diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 0b6354d05d..3877a2920f 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -322,7 +322,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// ImageFrame{TPixel}.CopyTo(): target must be of the same size! internal void CopyTo(Buffer2D target) { - if (this.Size() != target.Size()) + if (this.Size != target.Size()) { throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index e12631cbd7..1a50229ca9 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -419,9 +419,9 @@ public sealed class Image : Image ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames)); - Size rootSize = rootFrame.Size(); + Size rootSize = rootFrame.Size; - if (frames.Any(f => f.Size() != rootSize)) + if (frames.Any(f => f.Size != rootSize)) { throw new ArgumentException("The provided frames must be of the same size.", nameof(frames)); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index e4b0a60ab0..5931b7c402 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -96,7 +96,7 @@ internal class BokehBlurProcessor : ImageProcessor } // Create a 0-filled buffer to use to store the result of the component convolutions - using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size(), AllocationOptions.Clean); + using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size, AllocationOptions.Clean); // Perform the 1D convolutions on all the kernel components and accumulate the results this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer); @@ -134,7 +134,7 @@ internal class BokehBlurProcessor : ImageProcessor Buffer2D processingBuffer) { // Allocate the buffer with the intermediate convolution results - using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size); // Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width // to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation. diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index cc6e1e5fb2..10780a21e2 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -66,7 +66,7 @@ internal class Convolution2PassProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index d059ebe030..ae79f2c31d 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -51,7 +51,7 @@ internal class ConvolutionProcessor : ImageProcessor protected override void OnFrameApply(ImageFrame source) { MemoryAllocator allocator = this.Configuration.MemoryAllocator; - using Buffer2D targetPixels = allocator.Allocate2D(source.Size()); + using Buffer2D targetPixels = allocator.Allocate2D(source.Size); source.CopyTo(targetPixels); diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index 1491fe073b..f811bae0f7 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -38,7 +38,7 @@ internal class OilPaintingProcessor : ImageProcessor int levels = Math.Clamp(this.definition.Levels, 1, 255); int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height)); - using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); source.CopyTo(targetPixels); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ab49805a35..614a8a1f17 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -23,7 +23,6 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; [Theory] - [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) @@ -596,6 +595,16 @@ public class TiffDecoderTests : TiffDecoderBaseTester Assert.Equal(1, image.Frames.Count); } + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void CanDecode_MultiFrameMipMap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + image.DebugSaveMultiFrame(provider); + } + [Theory] [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs index dce6ebc38f..716b978a71 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -18,7 +18,6 @@ public class TiffEncoderMultiframeTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); [Theory] - [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs index aa8ab397d2..92fc06eff5 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -16,7 +15,7 @@ public class ExactImageComparer : ImageComparer ImageFrame expected, ImageFrame actual) { - if (expected.Size() != actual.Size()) + if (expected.Size != actual.Size) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index 93ed4c6fff..d057267da7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -58,7 +57,7 @@ public class TolerantImageComparer : ImageComparer public override ImageSimilarityReport CompareImagesOrFrames(int index, ImageFrame expected, ImageFrame actual) { - if (expected.Size() != actual.Size()) + if (expected.Size != actual.Size) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } From e8bbdf703ab55acb98f39320b22093f0f20558e1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 4 Aug 2024 15:29:06 +0200 Subject: [PATCH 38/43] Always set YcbcrSubSampling to 1:1 for TiffCompression.Jpeg, fixes issue #2679 --- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 7b0f439b1c..864452fd26 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -620,7 +620,12 @@ internal static class TiffDecoderOptionsParser } options.CompressionType = TiffDecoderCompressionType.OldJpeg; - AdjustOptionsYCbCrJpegCompression(options); + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } break; } @@ -628,7 +633,20 @@ internal static class TiffDecoderOptionsParser case TiffCompression.Jpeg: { options.CompressionType = TiffDecoderCompressionType.Jpeg; - AdjustOptionsYCbCrJpegCompression(options); + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } + + // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this, + // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679 + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + { + options.YcbcrSubSampling[0] = 1; + options.YcbcrSubSampling[1] = 1; + } break; } @@ -647,24 +665,6 @@ internal static class TiffDecoderOptionsParser } } - private static void AdjustOptionsYCbCrJpegCompression(TiffDecoderCore options) - { - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) - { - // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. - options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - options.ColorType = TiffColorType.Rgb; - } - - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) - { - // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this, - // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679 - options.YcbcrSubSampling[0] = 1; - options.YcbcrSubSampling[1] = 1; - } - } - private static bool IsBiColorCompression(TiffCompression? compression) { if (compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or From 978aff8208bd904b1bbd497f664e1389a7184d3b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 4 Aug 2024 17:22:57 +0200 Subject: [PATCH 39/43] Add test case for issue 2679 --- .../Formats/Tiff/TiffDecoderTests.cs | 17 +++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + ...de_JpegCompressedWithIssue2679_Issue2679.png | 3 +++ tests/Images/Input/Tiff/Issues/Issue2679.tiff | 3 +++ 4 files changed, 24 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png create mode 100644 tests/Images/Input/Tiff/Issues/Issue2679.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ab49805a35..97f02f3684 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -671,6 +671,23 @@ public class TiffDecoderTests : TiffDecoderBaseTester public void TiffDecoder_CanDecode_BiColorWithMissingBitsPerSample(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + // https://github.com/SixLabors/ImageSharp/issues/2679 + [Theory] + [WithFile(Issues2679, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_JpegCompressedWithIssue2679(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + + // The image is handcrafted to simulate issue 2679. ImageMagick will throw an expection here and wont decode, + // so we compare to rererence output instead. + image.DebugSave(provider); + image.CompareToReferenceOutput( + ImageComparer.Exact, + provider, + appendPixelTypeToFileName: false); + } + [Theory] [WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)] public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ac70792681..772437c4b0 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1032,6 +1032,7 @@ public static class TestImages public const string Issues2255 = "Tiff/Issues/Issue2255.png"; public const string Issues2435 = "Tiff/Issues/Issue2435.tiff"; public const string Issues2587 = "Tiff/Issues/Issue2587.tiff"; + public const string Issues2679 = "Tiff/Issues/Issue2679.tiff"; public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff"; public const string Tiled0000023664 = "Tiff/Issues/tiled-0000023664.tiff"; diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png new file mode 100644 index 0000000000..6150aacb37 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cd36c7e07a08e22cceecd28a056c80e5553a8c092bfc091e902d13bd5c46f4d +size 120054 diff --git a/tests/Images/Input/Tiff/Issues/Issue2679.tiff b/tests/Images/Input/Tiff/Issues/Issue2679.tiff new file mode 100644 index 0000000000..1bc49f079c --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/Issue2679.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb938396b9d5b4c258244197ba382937a52c93f72cc91081c7e6810e4a3b94c +size 6136 From 5c60126377aefbf089ccf365df0880d3006b3c63 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 4 Aug 2024 17:35:33 +0200 Subject: [PATCH 40/43] Add check, if YcbcrSubSampling has a value --- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 864452fd26..c3ff758ac1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -633,21 +633,22 @@ internal static class TiffDecoderOptionsParser case TiffCompression.Jpeg: { options.CompressionType = TiffDecoderCompressionType.Jpeg; - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) - { - // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. - options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - options.ColorType = TiffColorType.Rgb; - } // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this, // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679 - if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr) + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.YcbcrSubSampling != null) { options.YcbcrSubSampling[0] = 1; options.YcbcrSubSampling[1] = 1; } + if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) + { + // Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data. + options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + options.ColorType = TiffColorType.Rgb; + } + break; } From e1555fd4ba6a972b7b0121b22bf502ebb49a9606 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 5 Aug 2024 23:46:53 +1000 Subject: [PATCH 41/43] Fix transform bounds calculations --- .../Processing/AffineTransformBuilder.cs | 38 +-- .../Linear/LinearTransformUtility.cs | 4 +- .../Transforms/Linear/RotateProcessor.cs | 5 +- .../Transforms/Linear/SkewProcessor.cs | 5 +- .../Processors/Transforms/TransformUtils.cs | 274 +++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 39 +-- .../Transforms/AffineTransformTests.cs | 18 ++ .../Transforms/ProjectiveTransformTests.cs | 10 +- 8 files changed, 191 insertions(+), 202 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 59264698bd..e8c628ff12 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a rotation matrix using the given rotation angle in degrees @@ -31,8 +30,7 @@ public class AffineTransformBuilder /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -68,9 +66,7 @@ public class AffineTransformBuilder /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -145,9 +141,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -156,9 +150,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -187,9 +179,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -198,9 +188,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -267,7 +255,7 @@ public class AffineTransformBuilder public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -283,7 +271,7 @@ public class AffineTransformBuilder public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -340,13 +328,13 @@ public class AffineTransformBuilder // Translate the origin matrix to cater for source rectangle offsets. Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix3x2 matrix) @@ -357,17 +345,15 @@ public class AffineTransformBuilder } } - private AffineTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private AffineTransformBuilder Append(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs index b5eb202c18..1f68e32744 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs @@ -43,7 +43,7 @@ internal static class LinearTransformUtility /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeStart(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max); + => Numerics.Clamp((int)MathF.Floor(center - radius), min, max); /// /// Gets the end position (inclusive) for a sampling range given @@ -56,5 +56,5 @@ internal static class LinearTransformUtility /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeEnd(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Floor(center + radius), min, max); + => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index 6580636a24..aee7fd7816 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -29,14 +29,13 @@ public sealed class RotateProcessor : AffineTransformProcessor public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), - TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor - private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 97b18de6c8..085d2bbec1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -31,7 +31,6 @@ public sealed class SkewProcessor : AffineTransformProcessor public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), - TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize), sampler, sourceSize) { @@ -40,8 +39,8 @@ public sealed class SkewProcessor : AffineTransformProcessor } // Helper constructor: - private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 70112ab5a8..787e7fc3e3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -68,6 +68,11 @@ internal static class TransformUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) { + // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases, + // such as when the point is transformed behind the camera in a perspective projection. + // However, in many 2D contexts, negative w values are not meaningful and could cause issues + // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure + // we don't divide by a very small or negative number, effectively treating any negative w as epsilon. const float epsilon = 0.0000001F; Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); @@ -81,9 +86,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. @@ -93,33 +96,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size. - /// - /// The amount of rotation, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in radians and the source size. - /// - /// The amount of rotation, in radians. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -130,9 +107,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -143,78 +118,28 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); - - /// - /// Creates a centered skew bounds matrix from the give angles in degrees and the source size. - /// - /// The X angle, in degrees. - /// The Y angle, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); - - /// - /// Creates a centered skew bounds matrix from the give angles in radians and the source size. - /// - /// The X angle, in radians. - /// The Y angle, in radians. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); /// /// Gets the centered transform matrix based upon the source rectangle. /// - /// The source image bounds. - /// The transformation matrix. - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) - { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); - - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - // Centered transforms must be 0 based so we offset the bounds width and height. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F); - - // Translate back to world space. - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - return centered; - } - - /// - /// Gets the centered bounds matrix based upon the source rectangle. - /// - /// The source image bounds. /// The transformation matrix. + /// The source image size. /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredBoundsMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + Size destinationSize = GetUnboundedTransformedSize(matrix, size); // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + // The source size is provided using the coordinate space of the source image. + // however the transform should always be applied in the pixel space. + // To account for this we offset by the size - 1 to translate to the pixel space. + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -236,6 +161,12 @@ internal static class TransformUtils { Matrix4x4 matrix = Matrix4x4.Identity; + // The source size is provided using the Coordinate/Geometric space of the source image. + // However, the transform should always be applied in the Discrete/Pixel space to ensure + // that the transformation fully encompasses all pixels without clipping at the edges. + // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. + // size -= new Size(1, 1); + /* * SkMatrix is laid out in the following manner: * @@ -345,52 +276,101 @@ internal static class TransformUtils } /// - /// Returns the rectangle bounds relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) - { - Rectangle transformed = GetTransformedRectangle(rectangle, matrix); - return new Rectangle(0, 0, transformed.Width, transformed.Height); - } + public static Size GetTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, true); /// - /// Returns the rectangle relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size GetTransformedSize(Matrix4x4 matrix, Size size) { - if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) { - return rectangle; + return size; } - Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); - Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); - Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); - Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + // Check if the matrix involves only affine transformations by inspecting the relevant components. + // We want to use pixel space for calculations only if the transformation is purely 2D and does not include + // any perspective effects, non-standard scaling, or unusual translations that could distort the image. + // The conditions are as follows: + bool usePixelSpace = + + // 1. Ensure there's no perspective distortion: + // M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0. + (matrix.M34 == 0) && + + // 2. Ensure standard affine transformation without any unusual depth or perspective scaling: + // M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth + // scaling or perspective, which suggests a more complex transformation. + (matrix.M44 == 1) && + + // 3. Ensure no unusual translation in the x-direction: + // M14 represents translation in the x-direction that might be part of a more complex transformation. + // For standard affine transformations, M14 should be 0. + (matrix.M14 == 0) && - return GetBoundingRectangle(tl, tr, bl, br); + // 4. Ensure no unusual translation in the y-direction: + // M24 represents translation in the y-direction that might be part of a more complex transformation. + // For standard affine transformations, M24 should be 0. + (matrix.M24 == 0); + + // Define an offset size to translate between pixel space and coordinate space. + // When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates. + // When not using pixel space, use SizeF.Empty as the offset. + + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + + // Apply the offset relative to the scale + SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty; + + // Subtract the offset size to translate to the appropriate space (pixel or coordinate). + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the correct space. + return Size.Ceiling(ConstrainSize(bounds) + offsetSize); + } + + return size; } /// /// Returns the size relative to the source for the given transformation matrix. /// + /// The transformation matrix. /// The source size. + /// + /// The . + /// + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, false); + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// /// The transformation matrix. + /// The source size. + /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - public static Size GetTransformedSize(Size size, Matrix3x2 matrix) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -399,9 +379,20 @@ internal static class TransformUtils return size; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + // Define an offset size to translate between coordinate space and pixel space. + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + SizeF offsetSize = new(scaleX, scaleY); + + // Subtract the offset size to translate to the pixel space. + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the coordinate space. + return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize); + } - return ConstrainSize(rectangle); + return size; } /// @@ -409,46 +400,52 @@ internal static class TransformUtils /// /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) { - if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) { - return rectangle; + bounds = default; + return false; } - Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); - Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); - Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); - Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); + Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); - return GetBoundingRectangle(tl, tr, bl, br); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } /// - /// Returns the size relative to the source for the given transformation matrix. + /// Returns the rectangle relative to the source for the given transformation matrix. /// - /// The source size. + /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Size GetTransformedSize(Size size, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) { - Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); - - if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) { - return size; + bounds = default; + return false; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); + Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); + Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); + Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); - return ConstrainSize(rectangle); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -482,6 +479,11 @@ internal static class TransformUtils float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); - return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); + // Clamp the values to the nearest whole pixel. + return Rectangle.FromLTRB( + (int)Math.Floor(left), + (int)Math.Floor(top), + (int)Math.Ceiling(right), + (int)Math.Ceiling(bottom)); } } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 0387adebb9..06e6f0e71a 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a matrix that performs a tapering projective transform. @@ -22,9 +21,7 @@ public class ProjectiveTransformBuilder /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Prepend( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -34,9 +31,7 @@ public class ProjectiveTransformBuilder /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Append( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -52,9 +47,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -88,9 +81,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -174,9 +165,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -214,9 +203,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -283,7 +270,7 @@ public class ProjectiveTransformBuilder public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -299,7 +286,7 @@ public class ProjectiveTransformBuilder public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -357,13 +344,13 @@ public class ProjectiveTransformBuilder // Translate the origin matrix to cater for source rectangle offsets. Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix4x4 matrix) @@ -374,17 +361,15 @@ public class ProjectiveTransformBuilder } } - private ProjectiveTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private ProjectiveTransformBuilder Append(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index 895cf60fd3..05604ac6e6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -231,6 +231,24 @@ public class AffineTransformTests Assert.Equal(100, image.Height); } + [Theory] + [WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)] + public void Issue2753(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + AffineTransformBuilder builder = + new AffineTransformBuilder().AppendRotationDegrees(270, new Vector2(3.5f, 3.5f)); + image.Mutate(x => x.BackgroundColor(Color.Red)); + image.Mutate(x => x = x.Transform(builder)); + + image.DebugSave(provider); + + Assert.Equal(4, image.Width); + Assert.Equal(8, image.Height); + } + [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] public void Identity(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 21eda034ea..128df01a06 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -128,11 +128,11 @@ public class ProjectiveTransformTests using (Image image = provider.GetImage()) { #pragma warning disable SA1117 // Parameters should be on same line or separate lines - var matrix = new Matrix4x4( - 0.260987f, -0.434909f, 0, -0.0022184f, - 0.373196f, 0.949882f, 0, -0.000312129f, - 0, 0, 1, 0, - 52, 165, 0, 1); + Matrix4x4 matrix = new( + 0.260987f, -0.434909f, 0, -0.0022184f, + 0.373196f, 0.949882f, 0, -0.000312129f, + 0, 0, 1, 0, + 52, 165, 0, 1); #pragma warning restore SA1117 // Parameters should be on same line or separate lines ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() From 4d67be8b860ade92b20487aa6ece94283e22e661 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 9 Aug 2024 00:04:29 +1000 Subject: [PATCH 42/43] Can encode Tiff MipMaps --- .../Formats/AnimatedImageFrameMetadata.cs | 32 ---- .../Formats/AnimatedImageMetadata.cs | 33 ---- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 6 + src/ImageSharp/Formats/Cur/CurDecoderCore.cs | 4 - .../Formats/Cur/CurFrameMetadata.cs | 20 +++ src/ImageSharp/Formats/Cur/CurMetadata.cs | 32 +--- .../Formats/Gif/GifFrameMetadata.cs | 37 +---- src/ImageSharp/Formats/Gif/GifMetadata.cs | 6 + .../Formats/IFormatFrameMetadata.cs | 11 ++ src/ImageSharp/Formats/IFormatMetadata.cs | 8 + src/ImageSharp/Formats/Ico/IcoDecoderCore.cs | 2 - .../Formats/Ico/IcoFrameMetadata.cs | 20 +++ src/ImageSharp/Formats/Ico/IcoMetadata.cs | 20 +-- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 6 + src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 6 + .../Formats/Png/PngFrameMetadata.cs | 7 + src/ImageSharp/Formats/Png/PngMetadata.cs | 6 + src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 6 + src/ImageSharp/Formats/Tga/TgaMetadata.cs | 6 + .../Formats/Tiff/TiffDecoderCore.cs | 67 +++----- .../Formats/Tiff/TiffEncoderCore.cs | 18 +- .../Tiff/TiffEncoderEntriesCollector.cs | 10 +- .../Formats/Tiff/TiffFrameMetadata.cs | 155 +++++++++++++----- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 6 + .../Writers/TiffBaseColorWriter{TPixel}.cs | 27 ++- .../Tiff/Writers/TiffBiColorWriter{TPixel}.cs | 19 ++- .../Tiff/Writers/TiffColorWriterFactory.cs | 28 ++-- .../TiffCompositeColorWriter{TPixel}.cs | 21 +-- .../Tiff/Writers/TiffGrayL16Writer{TPixel}.cs | 12 +- .../Tiff/Writers/TiffGrayWriter{TPixel}.cs | 12 +- .../Tiff/Writers/TiffPaletteWriter{TPixel}.cs | 7 +- .../Tiff/Writers/TiffRgbWriter{TPixel}.cs | 12 +- .../Formats/Webp/WebpFrameMetadata.cs | 8 + src/ImageSharp/Formats/Webp/WebpMetadata.cs | 6 + src/ImageSharp/Image.cs | 8 +- src/ImageSharp/ImageFrame.cs | 12 +- src/ImageSharp/ImageFrame{TPixel}.cs | 22 ++- src/ImageSharp/Image{TPixel}.cs | 32 +++- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 33 +++- src/ImageSharp/Metadata/ImageMetadata.cs | 16 ++ .../Metadata/Profiles/Exif/ExifProfile.cs | 14 ++ .../CloningImageProcessor{TPixel}.cs | 6 +- .../Transforms/TransformProcessorHelpers.cs | 39 ----- ...essor.cs => TransformProcessor{TPixel}.cs} | 9 +- .../Formats/Tiff/TiffEncoderTests.cs | 77 +++++++++ .../Transforms/TransformsHelpersTest.cs | 35 ---- 46 files changed, 598 insertions(+), 381 deletions(-) delete mode 100644 src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs delete mode 100644 src/ImageSharp/Formats/AnimatedImageMetadata.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs rename src/ImageSharp/Processing/Processors/Transforms/{TransformProcessor.cs => TransformProcessor{TPixel}.cs} (80%) delete mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs deleted file mode 100644 index 8f8e187403..0000000000 --- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats; - -internal class AnimatedImageFrameMetadata -{ - /// - /// Gets or sets the frame color table. - /// - public ReadOnlyMemory? ColorTable { get; set; } - - /// - /// Gets or sets the frame color table mode. - /// - public FrameColorTableMode ColorTableMode { get; set; } - - /// - /// Gets or sets the duration of the frame. - /// - public TimeSpan Duration { get; set; } - - /// - /// Gets or sets the frame alpha blending mode. - /// - public FrameBlendMode BlendMode { get; set; } - - /// - /// Gets or sets the frame disposal mode. - /// - public FrameDisposalMode DisposalMode { get; set; } -} diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs deleted file mode 100644 index ac3ca29f4f..0000000000 --- a/src/ImageSharp/Formats/AnimatedImageMetadata.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats; - -internal class AnimatedImageMetadata -{ - /// - /// Gets or sets the shared color table. - /// - public ReadOnlyMemory? ColorTable { get; set; } - - /// - /// Gets or sets the shared color table mode. - /// - public FrameColorTableMode ColorTableMode { get; set; } - - /// - /// Gets or sets the default background color of the canvas when animating. - /// This color may be used to fill the unused space on the canvas around the frames, - /// as well as the transparent pixels of the first frame. - /// The background color is also used when the disposal mode is . - /// - public Color BackgroundColor { get; set; } - - /// - /// Gets or sets the number of times any animation is repeated. - /// - /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1. - /// - /// - public ushort RepeatCount { get; set; } -} diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 68e99bdc5f..d0c60421c4 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata /// public BmpMetadata DeepClone() => new(this); + + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } } diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index a8a51878e0..6fc8905279 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore curMetadata.Compression = compression; curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.ColorTable = colorTable; - curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; - curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; - curMetadata.HotspotX = curFrameMetadata.HotspotX; - curMetadata.HotspotY = curFrameMetadata.HotspotY; } } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 06cf426dc4..4e9a432b16 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata EncodingHeight = this.EncodingHeight }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata ColorType = color }; } + + private static byte Scale(byte? value, int destination, float ratio) + { + if (value is null) + { + return (byte)Math.Clamp(destination, 0, 255); + } + + return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + } } diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index 6e97a8584a..19de7f434d 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs @@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata private CurMetadata(CurMetadata other) { this.Compression = other.Compression; - this.HotspotX = other.HotspotX; - this.HotspotY = other.HotspotY; - this.EncodingWidth = other.EncodingWidth; - this.EncodingHeight = other.EncodingHeight; this.BmpBitsPerPixel = other.BmpBitsPerPixel; if (other.ColorTable?.Length > 0) @@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata /// public IconFrameCompression Compression { get; set; } - /// - /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame. - /// - public ushort HotspotX { get; set; } - - /// - /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame. - /// - public ushort HotspotY { get; set; } - - /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- 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 or greater. Derived from the root frame. - ///
- public byte EncodingHeight { get; set; } - /// /// Gets or sets the number of bits per pixel.
/// Used when is @@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata ColorTable = this.ColorTable }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index f81329e973..5fe892c656 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata }; } + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// public GifFrameMetadata DeepClone() => new(this); - - internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) - { - // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table? - int index = -1; - const float background = 1f; - if (metadata.ColorTable.HasValue) - { - ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; - for (int i = 0; i < colorTable.Length; i++) - { - Vector4 vector = colorTable[i].ToScaledVector4(); - if (vector.W < background) - { - index = i; - } - } - } - - bool hasTransparency = index >= 0; - - return new() - { - LocalColorTable = metadata.ColorTable, - ColorTableMode = metadata.ColorTableMode, - FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMode = metadata.DisposalMode, - HasTransparency = hasTransparency, - TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, - }; - } } diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 565038b55a..517609af45 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata }; } + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs index 4eef93ad34..20f27d050c 100644 --- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs +++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats; /// @@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable /// /// The . FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel; } /// diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs index 8d695306e4..a351431c94 100644 --- a/src/ImageSharp/Formats/IFormatMetadata.cs +++ b/src/ImageSharp/Formats/IFormatMetadata.cs @@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable /// /// The . FormatConnectingMetadata ToFormatConnectingMetadata(); + + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image . + void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel; } /// diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index 8b59974eb3..b8a1dded15 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore curMetadata.Compression = compression; curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.ColorTable = colorTable; - curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; - curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; } } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index c244e38981..a2d1c01391 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata EncodingHeight = this.EncodingHeight }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata ColorType = color }; } + + private static byte Scale(byte? value, int destination, float ratio) + { + if (value is null) + { + return (byte)Math.Clamp(destination, 0, 255); + } + + return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + } } diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index 7e31468ecc..a6c2704b31 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata private IcoMetadata(IcoMetadata other) { this.Compression = other.Compression; - this.EncodingWidth = other.EncodingWidth; - this.EncodingHeight = other.EncodingHeight; this.BmpBitsPerPixel = other.BmpBitsPerPixel; if (other.ColorTable?.Length > 0) @@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata /// public IconFrameCompression Compression { get; set; } - /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- 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 or greater. Derived from the root frame. - ///
- public byte EncodingHeight { get; set; } - /// /// Gets or sets the number of bits per pixel.
/// Used when is @@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata ColorTable = this.ColorTable }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index f2f34ec496..fe4855dc77 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata Quality = this.Quality, }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index fec4beda7c..d852f3c8eb 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo(), }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index c142a1c8e0..b8086cd6d1 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata }; } + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index a7b3672ef5..00cba088cb 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index e2062014d7..e463d511d2 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 58b5119523..8d40f86464 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 0ba9755b15..d699a7b631 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -211,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore IList directories = reader.Read(); List framesMetadata = []; - foreach (ExifProfile dir in directories) + int width = 0; + int height = 0; + + for (int i = 0; i < directories.Count; i++) { - framesMetadata.Add(this.CreateFrameMetadata(dir)); - } + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta + = this.CreateFrameMetadata(directories[i]); - ExifProfile rootFrameExifProfile = directories[0]; + framesMetadata.Add(meta.FrameMetadata); - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); + width = Math.Max(width, meta.TiffMetadata.EncodingWidth); + height = Math.Max(height, meta.TiffMetadata.EncodingHeight); + } - int width = GetImageWidth(rootFrameExifProfile); - int height = GetImageHeight(rootFrameExifProfile); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); return new ImageInfo(new(width, height), metadata, framesMetadata); } @@ -237,11 +241,11 @@ internal class TiffDecoderCore : ImageDecoderCore private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); - bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata()); + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags); + bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata); - int width = GetImageWidth(tags); - int height = GetImageHeight(tags); + int width = metadata.TiffFrameMetadata.EncodingWidth; + int height = metadata.TiffFrameMetadata.EncodingHeight; // If size has a value and the width/height off the tiff is smaller we much capture the delta. if (size.HasValue) @@ -256,7 +260,7 @@ internal class TiffDecoderCore : ImageDecoderCore size = new Size(width, height); } - ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, imageFrameMetaData); + ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata); if (isTiled) { @@ -270,7 +274,7 @@ internal class TiffDecoderCore : ImageDecoderCore return frame; } - private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags) + private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags) { ImageFrameMetadata imageFrameMetaData = new(); if (!this.skipMetadata) @@ -278,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore imageFrameMetaData.ExifProfile = tags; } - TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags); + TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags); + imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata); - return imageFrameMetaData; + return (imageFrameMetaData, tiffMetadata); } /// @@ -825,38 +830,6 @@ internal class TiffDecoderCore : ImageDecoderCore return bytesPerRow * height; } - /// - /// Gets the width of the image frame. - /// - /// The image frame exif profile. - /// The image width. - private static int GetImageWidth(ExifProfile exifProfile) - { - if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue width)) - { - TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth"); - } - - DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); - - return (int)width.Value; - } - - /// - /// Gets the height of the image frame. - /// - /// The image frame exif profile. - /// The image height. - private static int GetImageHeight(ExifProfile exifProfile) - { - if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue height)) - { - TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); - } - - return (int)height.Value; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 5f91fd7393..b560067f3f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore long ifdOffset) where TPixel : unmanaged, IPixel { + // Get the width and height of the frame. + // This can differ from the frame bounds in-memory if the image represents only + // a subregion. + TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata(); + int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width; + int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height; + + width = Math.Min(width, frame.Width); + height = Math.Min(height, frame.Height); + Size encodingSize = new(width, height); + using TiffBaseCompressor compressor = TiffCompressorFactory.Create( compression, writer.BaseStream, this.memoryAllocator, - frame.Width, + width, (int)bitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); @@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, frame, + encodingSize, this.quantizer, this.pixelSamplingStrategy, this.memoryAllocator, @@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore entriesCollector, (int)bitsPerPixel); - int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); + int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType); colorWriter.Write(compressor, rowsPerStrip); @@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore // Write the metadata for the frame entriesCollector.ProcessMetadata(frame, this.skipMetadata); - entriesCollector.ProcessFrameInfo(frame, imageMetadata); + entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata); entriesCollector.ProcessImageFormat(this); if (writer.Position % 2 != 0) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index c8e28111ec..803b77fb0a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector public void ProcessMetadata(ImageFrame frame, bool skipMetadata) => new MetadataProcessor(this).Process(frame, skipMetadata); - public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) - => new FrameInfoProcessor(this).Process(frame, imageMetadata); + public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector { } - public void Process(ImageFrame frame, ImageMetadata imageMetadata) + public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) { this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) { - Value = (uint)frame.Width + Value = (uint)encodingSize.Width }); this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) { - Value = (uint)frame.Height + Value = (uint)encodingSize.Height }); this.ProcessResolution(imageMetadata); diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index bb5da37411..189fee8b0c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata this.PhotometricInterpretation = other.PhotometricInterpretation; this.Predictor = other.Predictor; this.InkSet = other.InkSet; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; } /// @@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata /// public TiffInkSet? InkSet { get; set; } + /// + /// Gets or sets the encoding width. + /// + public int EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height. + /// + public int EncodingHeight { get; set; } + /// public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) - => new(); + { + TiffFrameMetadata frameMetadata = new(); + if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue) + { + frameMetadata.EncodingWidth = metadata.EncodingWidth.Value; + frameMetadata.EncodingHeight = metadata.EncodingHeight.Value; + } + + return frameMetadata; + } /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => new(); + => new() + { + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + + // Overwrite the EXIF dimensional metadata with the encoding dimensions of the image. + destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight); + } + + private static int Scale(int value, int destination, float ratio) + { + if (value <= 0) + { + return destination; + } + + return Math.Min((int)MathF.Ceiling(value * ratio), destination); + } /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata /// /// The tiff frame meta data. /// The Exif profile containing tiff frame directory tags. - internal static void Parse(TiffFrameMetadata meta, ExifProfile profile) + private static void Parse(TiffFrameMetadata meta, ExifProfile profile) { - if (profile != null) + meta.EncodingWidth = GetImageWidth(profile); + meta.EncodingHeight = GetImageHeight(profile); + + if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue) + && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)) + { + meta.BitsPerSample = bitsPerSample; + } + + meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); + + if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) { - if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue) - && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)) - { - meta.BitsPerSample = bitsPerSample; - } - - meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); - - if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) - { - meta.Compression = (TiffCompression)compressionValue.Value; - } - - if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue)) - { - meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value; - } - - if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue)) - { - meta.Predictor = (TiffPredictor)predictorValue.Value; - } - - if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue)) - { - meta.InkSet = (TiffInkSet)inkSetValue.Value; - } - - // TODO: Why do we remove this? Encoding should overwrite. - profile.RemoveValue(ExifTag.BitsPerSample); - profile.RemoveValue(ExifTag.Compression); - profile.RemoveValue(ExifTag.PhotometricInterpretation); - profile.RemoveValue(ExifTag.Predictor); + meta.Compression = (TiffCompression)compressionValue.Value; } + + if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue)) + { + meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value; + } + + if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue)) + { + meta.Predictor = (TiffPredictor)predictorValue.Value; + } + + if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue)) + { + meta.InkSet = (TiffInkSet)inkSetValue.Value; + } + + // Remove values, we've explicitly captured them and they could change on encode. + profile.RemoveValue(ExifTag.BitsPerSample); + profile.RemoveValue(ExifTag.Compression); + profile.RemoveValue(ExifTag.PhotometricInterpretation); + profile.RemoveValue(ExifTag.Predictor); + } + + /// + /// Gets the width of the image frame. + /// + /// The image frame exif profile. + /// The image width. + private static int GetImageWidth(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue? width)) + { + TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth"); + } + + DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); + + return (int)width.Value; + } + + /// + /// Gets the height of the image frame. + /// + /// The image frame exif profile. + /// The image height. + private static int GetImageHeight(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue? height)) + { + TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); + } + + return (int)height.Value; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index cc70941d51..e965fcb4f6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs index c4a7492553..9fd730f416 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs @@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter : IDisposable { private bool isDisposed; - protected TiffBaseColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + protected TiffBaseColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) { + this.Width = encodingSize.Width; + this.Height = encodingSize.Height; this.Image = image; this.MemoryAllocator = memoryAllocator; this.Configuration = configuration; @@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter : IDisposable ///
public abstract int BitsPerPixel { get; } + /// + /// Gets the width of the portion of the image to be encoded. + /// + public int Width { get; } + + /// + /// Gets the height of the portion of the image to be encoded. + /// + public int Height { get; } + /// /// Gets the bytes per row. /// - public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8); + public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8); protected ImageFrame Image { get; } @@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter : IDisposable public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) { DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer"); - int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip; + int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip; uint[] stripOffsets = new uint[stripsCount]; uint[] stripByteCounts = new uint[stripsCount]; int stripIndex = 0; compressor.Initialize(rowsPerStrip); - for (int y = 0; y < this.Image.Height; y += rowsPerStrip) + for (int y = 0; y < this.Height; y += rowsPerStrip) { long offset = compressor.Output.Position; - int height = Math.Min(rowsPerStrip, this.Image.Height - y); + int height = Math.Min(rowsPerStrip, this.Height - y); this.EncodeStrip(y, height, compressor); long endOffset = compressor.Output.Position; diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index a6f4c31060..647ff8a1a3 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter private IMemoryOwner bitStrip; - public TiffBiColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffBiColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { // Convert image to black and white. - this.imageBlackWhite = new Image(configuration, new ImageMetadata(), new[] { image.Clone() }); + this.imageBlackWhite = new Image(configuration, new ImageMetadata(), [image.Clone()]); this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); } @@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter /// protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - int width = this.Image.Width; + int width = this.Width; - if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax) + if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax) { // Special case for T4BitCompressor. int stripPixels = width * height; @@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter int bitIndex = 0; int byteIndex = 0; Span outputRow = rows[(outputRowIdx * this.BytesPerRow)..]; - Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row); + Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width]; PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); - for (int x = 0; x < this.Image.Width; x++) + for (int x = 0; x < this.Width; x++) { int shift = 7 - bitIndex; if (pixelAsGraySpan[x] == 255) diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs index 96c8aeb324..31a1b0e414 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory public static TiffBaseColorWriter Create( TiffPhotometricInterpretation? photometricInterpretation, ImageFrame image, + Size encodingSize, IQuantizer quantizer, IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, @@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) where TPixel : unmanaged, IPixel - { - switch (photometricInterpretation) + => photometricInterpretation switch { - case TiffPhotometricInterpretation.PaletteColor: - return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); - case TiffPhotometricInterpretation.BlackIsZero: - case TiffPhotometricInterpretation.WhiteIsZero: - return bitsPerPixel switch - { - 1 => new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector), - 16 => new TiffGrayL16Writer(image, memoryAllocator, configuration, entriesCollector), - _ => new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector) - }; - - default: - return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector); - } - } + TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel), + TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch + { + 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector), + _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector) + }, + _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + }; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs index 007857148a..67dde493c5 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs @@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; /// /// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr). /// +/// The tpe of pixel format. internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter where TPixel : unmanaged, IPixel { private IMemoryOwner rowBuffer; - protected TiffCompositeColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + protected TiffCompositeColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - if (this.rowBuffer == null) - { - this.rowBuffer = this.MemoryAllocator.Allocate(this.BytesPerRow * height); - } - - this.rowBuffer.Clear(); + (this.rowBuffer ??= this.MemoryAllocator.Allocate(this.BytesPerRow * height)).Clear(); Span outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)]; - int width = this.Image.Width; + int width = this.Width; using IMemoryOwner stripPixelBuffer = this.MemoryAllocator.Allocate(height * width); Span stripPixels = stripPixelBuffer.GetSpan(); int lastRow = y + height; int stripPixelsRowIdx = 0; for (int row = y; row < lastRow; row++) { - Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row); + Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width]; stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); stripPixelsRowIdx++; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs index 3e0e074e95..857f551f41 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffGrayL16Writer(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffGrayL16Writer( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter 16; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs index b2a476b9aa..4a037f0d33 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffGrayWriter : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffGrayWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffGrayWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter : TiffCompositeColorWriter public override int BitsPerPixel => 8; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs index d9a0960d9b..da66373631 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter public TiffPaletteWriter( ImageFrame frame, + Size encodingSize, IQuantizer quantizer, IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) - : base(frame, memoryAllocator, configuration, entriesCollector) + : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector) { DebugGuard.NotNull(quantizer, nameof(quantizer)); DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); @@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter }); frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); - this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize)); this.AddColorMapTag(); } @@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter /// protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - int width = this.Image.Width; + int width = this.quantizedFrame.Width; if (this.BitsPerPixel == 4) { diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs index 3494b6ceae..93c46a92e4 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffRgbWriter : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffRgbWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffRgbWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter : TiffCompositeColorWriter public override int BitsPerPixel => 24; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index 45e182d223..3865f9837f 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Webp; /// @@ -61,6 +63,12 @@ public class WebpFrameMetadata : IFormatFrameMetadata BlendMode = this.BlendMethod, }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 33ebbbf6dc..db57bd8f27 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -145,6 +145,12 @@ public class WebpMetadata : IFormatMetadata BackgroundColor = this.BackgroundColor }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 9a5bd4cf06..07b40a41a1 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -72,7 +72,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// /// Gets any metadata associated with the image. /// - public ImageMetadata Metadata { get; } + public ImageMetadata Metadata { get; private set; } /// /// Gets the size of the image in px units. @@ -185,6 +185,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The . protected void UpdateSize(Size size) => this.Size = size; + /// + /// Updates the metadata of the image after mutation. + /// + /// The . + protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata; + /// /// Disposes the object and frees resources for the Garbage Collector. /// diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 3c5338fac3..fdde5019e1 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -42,7 +42,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Gets the metadata of the frame. /// - public ImageFrameMetadata Metadata { get; } + public ImageFrameMetadata Metadata { get; private set; } /// public Configuration Configuration { get; } @@ -75,8 +75,14 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable where TDestinationPixel : unmanaged, IPixel; /// - /// Updates the size of the image frame. + /// Updates the size of the image frame after mutation. /// - /// The size. + /// The . protected void UpdateSize(Size size) => this.Size = size; + + /// + /// Updates the metadata of the image frame after mutation. + /// + /// The . + protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata; } diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 3877a2920f..2287f65cd8 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -331,17 +331,29 @@ public sealed class ImageFrame : ImageFrame, IPixelSource } /// - /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. + /// Switches the buffers used by the image and the pixel source meaning that the Image will "own" the buffer + /// from the pixelSource and the pixel source will now own the Image buffer. /// - /// The pixel source. - internal void SwapOrCopyPixelsBufferFrom(ImageFrame pixelSource) + /// The pixel source. + internal void SwapOrCopyPixelsBufferFrom(ImageFrame source) { - Guard.NotNull(pixelSource, nameof(pixelSource)); + Guard.NotNull(source, nameof(source)); - Buffer2D.SwapOrCopyContent(this.PixelBuffer, pixelSource.PixelBuffer); + Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); this.UpdateSize(this.PixelBuffer.Size()); } + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + this.UpdateMetadata(source.Metadata); + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 1a50229ca9..02403923d2 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -395,22 +395,42 @@ public sealed class Image : Image } /// - /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. + /// Switches the buffers used by the image and the pixel source meaning that the Image will + /// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer. /// - /// The pixel source. - internal void SwapOrCopyPixelsBuffersFrom(Image pixelSource) + /// The pixel source. + internal void SwapOrCopyPixelsBuffersFrom(Image source) { - Guard.NotNull(pixelSource, nameof(pixelSource)); + Guard.NotNull(source, nameof(source)); this.EnsureNotDisposed(); - ImageFrameCollection sourceFrames = pixelSource.Frames; + ImageFrameCollection sourceFrames = source.Frames; for (int i = 0; i < this.frames.Count; i++) { this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]); } - this.UpdateSize(pixelSource.Size); + this.UpdateSize(source.Size); + } + + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(Image source) + { + Guard.NotNull(source, nameof(source)); + + this.EnsureNotDisposed(); + + ImageFrameCollection sourceFrames = source.Frames; + for (int i = 0; i < this.frames.Count; i++) + { + this.frames[i].CopyMetadataFrom(sourceFrames[i]); + } + + this.UpdateMetadata(source.Metadata); } private static Size ValidateFramesAndGetSize(IEnumerable> frames) diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 9c0de1edbe..b24aa140fc 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Metadata; @@ -110,16 +111,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) { TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); - this.formatMetadata[key] = derivedMeta; + this.SetFormatMetadata(key, derivedMeta); return derivedMeta; } TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); - this.formatMetadata[key] = newMeta; + this.SetFormatMetadata(key, newMeta); return newMeta; } - internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) + /// + /// Sets the metadata value associated with the specified key. + /// + /// The type of format metadata. + /// The type of format frame metadata. + /// The key of the value to set. + /// The value to set. + public void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) where TFormatMetadata : class where TFormatFrameMetadata : class, IFormatFrameMetadata => this.formatMetadata[key] = value; @@ -143,4 +151,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable /// Synchronizes the profiles with the current metadata. /// internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + internal void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + // Always updated using the full frame dimensions. + // Individual format frame metadata will update with sub region dimensions if appropriate. + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterFrameApply(source, destination); + } + } } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 37557ba1dc..1961dbf192 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -230,6 +230,22 @@ public sealed class ImageMetadata : IDeepCloneable /// internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image. + internal void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterImageApply(destination); + } + } + internal PixelTypeInfo GetDecodedPixelTypeInfo() { // None found. Check if we have a decoded format to convert from. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 41d3c293b6..e91a69444d 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.PixelFormats; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -298,6 +299,19 @@ public sealed class ExifProfile : IDeepCloneable this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); } + internal void SyncDimensions(int width, int height) + { + if (this.TryGetValue(ExifTag.PixelXDimension, out _)) + { + this.SetValue(ExifTag.PixelXDimension, width); + } + + if (this.TryGetValue(ExifTag.PixelYDimension, out _)) + { + this.SetValue(ExifTag.PixelYDimension, height); + } + } + /// /// Synchronizes the profiles with the specified metadata. /// diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs index aa000a10e7..abe32e3882 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs @@ -48,7 +48,6 @@ public abstract class CloningImageProcessor : ICloningImageProcessor clone = this.CreateTarget(); this.CheckFrameCount(this.Source, clone); - Configuration configuration = this.Configuration; this.BeforeImageApply(clone); for (int i = 0; i < this.Source.Frames.Count; i++) @@ -77,9 +76,10 @@ public abstract class CloningImageProcessor : ICloningImageProcessor)this).CloneAndExecute(); - // We now need to move the pixel data/size data from the clone to the source. + // We now need to move the pixel data/size data and any metadata from the clone to the source. this.CheckFrameCount(this.Source, clone); this.Source.SwapOrCopyPixelsBuffersFrom(clone); + this.Source.CopyMetadataFrom(clone); } finally { @@ -157,7 +157,7 @@ public abstract class CloningImageProcessor : ICloningImageProcessor[source.Frames.Count]; + ImageFrame[] destinationFrames = new ImageFrame[source.Frames.Count]; for (int i = 0; i < destinationFrames.Length; i++) { destinationFrames[i] = new ImageFrame( diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs deleted file mode 100644 index 0bb4920f0f..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms; - -/// -/// Contains helper methods for working with transforms. -/// -internal static class TransformProcessorHelpers -{ - /// - /// Updates the dimensional metadata of a transformed image - /// - /// The pixel format. - /// The image to update - public static void UpdateDimensionalMetadata(Image image) - where TPixel : unmanaged, IPixel - { - ExifProfile? profile = image.Metadata.ExifProfile; - if (profile is null) - { - return; - } - - // Only set the value if it already exists. - if (profile.TryGetValue(ExifTag.PixelXDimension, out _)) - { - profile.SetValue(ExifTag.PixelXDimension, image.Width); - } - - if (profile.TryGetValue(ExifTag.PixelYDimension, out _)) - { - profile.SetValue(ExifTag.PixelYDimension, image.Height); - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs similarity index 80% rename from src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs index 0c2c29391b..bdfac00366 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs @@ -23,10 +23,17 @@ internal abstract class TransformProcessor : CloningImageProcessor + protected override void AfterFrameApply(ImageFrame source, ImageFrame destination) + { + base.AfterFrameApply(source, destination); + destination.Metadata.AfterFrameApply(source, destination); + } + /// protected override void AfterImageApply(Image destination) { - TransformProcessorHelpers.UpdateDimensionalMetadata(destination); base.AfterImageApply(destination); + destination.Metadata.AfterImageApply(destination); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 1972101164..0d59625ca7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff; @@ -292,6 +293,82 @@ public class TiffEncoderTests : TiffEncoderBaseTester Assert.Equal(expectedCompression, frameMetaData.Compression); } + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void TiffEncoder_EncodesMultiFrameMipMap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + + using MemoryStream memStream = new(); + image.SaveAsTiff(memStream); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + + Assert.Equal(image.Size, output.Size); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + for (int i = 0; i < image.Frames.Count; i++) + { + TiffFrameMetadata inputMetadata = image.Frames[i].Metadata.GetTiffMetadata(); + TiffFrameMetadata outputMetadata = output.Frames[i].Metadata.GetTiffMetadata(); + + Assert.Equal(inputMetadata.EncodingWidth, outputMetadata.EncodingWidth); + Assert.Equal(inputMetadata.EncodingHeight, outputMetadata.EncodingHeight); + } + } + + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void TiffEncoder_EncodesMultiFrameMipMap_WithScaling(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + + Size size = image.Size; + + List encodedDimensions = []; + foreach (ImageFrame frame in image.Frames) + { + TiffFrameMetadata metadata = frame.Metadata.GetTiffMetadata(); + encodedDimensions.Add(new Size(metadata.EncodingWidth, metadata.EncodingHeight)); + } + + const int scale = 2; + image.Mutate(x => x.Resize(image.Width / scale, image.Height / scale)); + + using MemoryStream memStream = new(); + image.SaveAsTiff(memStream); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + + Assert.Equal(image.Size, output.Size); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + // The encoded dimensions should automatically be scaled down by the + // horizontal and vertical scaling factors. + float ratioX = output.Width / (float)size.Width; + float ratioY = output.Height / (float)size.Height; + + for (int i = 0; i < image.Frames.Count; i++) + { + TiffFrameMetadata inputMetadata = image.Frames[i].Metadata.GetTiffMetadata(); + TiffFrameMetadata outputMetadata = output.Frames[i].Metadata.GetTiffMetadata(); + + int expectedWidth = (int)MathF.Ceiling(encodedDimensions[i].Width * ratioX); + int expectedHeight = (int)MathF.Ceiling(encodedDimensions[i].Height * ratioY); + + Assert.Equal(expectedWidth, inputMetadata.EncodingWidth); + Assert.Equal(expectedHeight, inputMetadata.EncodingHeight); + Assert.Equal(inputMetadata.EncodingWidth, outputMetadata.EncodingWidth); + Assert.Equal(inputMetadata.EncodingHeight, outputMetadata.EncodingHeight); + } + } + // This makes sure, that when decoding a planar tiff, the planar configuration is not carried over to the encoded image. [Theory] [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs deleted file mode 100644 index 21b92a01e8..0000000000 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; -using SixLabors.ImageSharp.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Tests.Processing.Transforms; - -[Trait("Category", "Processors")] -public class TransformsHelpersTest -{ - [Fact] - public void HelperCanChangeExifDataType() - { - int xy = 1; - - using (var img = new Image(xy, xy)) - { - var profile = new ExifProfile(); - img.Metadata.ExifProfile = profile; - profile.SetValue(ExifTag.PixelXDimension, xy + ushort.MaxValue); - profile.SetValue(ExifTag.PixelYDimension, xy + ushort.MaxValue); - - Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType); - Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType); - - TransformProcessorHelpers.UpdateDimensionalMetadata(img); - - Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType); - Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType); - } - } -} From c579547a4f0987d5defbf425be4301475c4a52d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 13 Aug 2024 22:29:47 +1000 Subject: [PATCH 43/43] Separate transform spaces --- .../Processing/AffineTransformBuilder.cs | 36 ++++++++-- .../Transforms/Linear/RotateProcessor.cs | 4 +- .../Transforms/Linear/SkewProcessor.cs | 4 +- .../Processors/Transforms/TransformUtils.cs | 70 ++++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 30 ++++++-- src/ImageSharp/Processing/TransformSpace.cs | 26 +++++++ .../Transforms/TransformBuilderTestBase.cs | 5 +- ...tPattern100x50_R(50)_S(1,1)_T(-20,-10).png | 4 +- ..._TestPattern100x50_R(50)_S(1,1)_T(0,0).png | 4 +- ...estPattern100x50_R(50)_S(1,1)_T(20,10).png | 4 +- ...ttern100x50_R(50)_S(1.1,1.3)_T(30,-20).png | 4 +- ...tPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png | 4 +- ...d_Rgba32_TestPattern96x96_R(50)_S(0.8).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- .../DrawImageTests/DrawTransformed.png | 4 +- ...sCSS_Rgba32_Solid290x154_(0,0,255,255).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...2_Solid30x30_(255,0,0,255)_Bottom-Both.png | 4 +- ...id30x30_(255,0,0,255)_Bottom-LeftOrTop.png | 4 +- ...x30_(255,0,0,255)_Bottom-RightOrBottom.png | 4 +- ...a32_Solid30x30_(255,0,0,255)_Left-Both.png | 4 +- ...olid30x30_(255,0,0,255)_Left-LeftOrTop.png | 4 +- ...30x30_(255,0,0,255)_Left-RightOrBottom.png | 4 +- ...32_Solid30x30_(255,0,0,255)_Right-Both.png | 4 +- ...lid30x30_(255,0,0,255)_Right-LeftOrTop.png | 4 +- ...0x30_(255,0,0,255)_Right-RightOrBottom.png | 4 +- ...ba32_Solid30x30_(255,0,0,255)_Top-Both.png | 4 +- ...Solid30x30_(255,0,0,255)_Top-LeftOrTop.png | 4 +- ...d30x30_(255,0,0,255)_Top-RightOrBottom.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- ...otate_WithAngle_TestPattern100x50_-170.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_-50.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_170.png | 4 +- .../Rotate_WithAngle_TestPattern100x50_50.png | 4 +- ...otate_WithAngle_TestPattern50x100_-170.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_-50.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_170.png | 4 +- .../Rotate_WithAngle_TestPattern50x100_50.png | 4 +- ...lType_Bgra32_TestPattern100x50_-20_-10.png | 4 +- ...xelType_Bgra32_TestPattern100x50_20_10.png | 4 +- ...elType_Rgb24_TestPattern100x50_-20_-10.png | 4 +- ...ixelType_Rgb24_TestPattern100x50_20_10.png | 4 +- 75 files changed, 262 insertions(+), 185 deletions(-) create mode 100644 src/ImageSharp/Processing/TransformSpace.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index e8c628ff12..4ac9546f39 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -13,6 +13,28 @@ public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public AffineTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the affine transform. + /// + public AffineTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the affine transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a rotation matrix using the given rotation angle in degrees /// and the image center point as rotation center. @@ -30,7 +52,7 @@ public class AffineTransformBuilder /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -66,7 +88,7 @@ public class AffineTransformBuilder /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -141,7 +163,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -150,7 +172,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -179,7 +201,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -188,7 +210,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -334,7 +356,7 @@ public class AffineTransformBuilder CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(matrix, size); + return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace); } private static void CheckDegenerate(Matrix3x2 matrix) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index aee7fd7816..0af2b268a1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -28,14 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), + TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 085d2bbec1..0bbc8e0f60 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -30,7 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), + TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel), sampler, sourceSize) { @@ -40,7 +40,7 @@ public sealed class SkewProcessor : AffineTransformProcessor // Helper constructor: private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 787e7fc3e3..62ea5e830d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -83,20 +83,22 @@ internal static class TransformUtils /// /// The amount of rotation, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace) + => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. /// /// The amount of rotation, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -104,10 +106,11 @@ internal static class TransformUtils /// The X angle, in degrees. /// The Y angle, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace) + => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -115,21 +118,25 @@ internal static class TransformUtils /// The X angle, in radians. /// The Y angle, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace); /// /// Gets the centered transform matrix based upon the source rectangle. /// /// The transformation matrix. /// The source image size. + /// + /// The to use when creating the centered matrix. + /// /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace) { - Size destinationSize = GetUnboundedTransformedSize(matrix, size); + Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace); // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. @@ -138,8 +145,10 @@ internal static class TransformUtils // The source size is provided using the coordinate space of the source image. // however the transform should always be applied in the pixel space. // To account for this we offset by the size - 1 to translate to the pixel space. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); + float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F; + + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -161,12 +170,6 @@ internal static class TransformUtils { Matrix4x4 matrix = Matrix4x4.Identity; - // The source size is provided using the Coordinate/Geometric space of the source image. - // However, the transform should always be applied in the Discrete/Pixel space to ensure - // that the transformation fully encompasses all pixels without clipping at the edges. - // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. - // size -= new Size(1, 1); - /* * SkMatrix is laid out in the following manner: * @@ -280,11 +283,10 @@ internal static class TransformUtils /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - public static Size GetTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, true); + /// The to use when calculating the size. + /// The . + public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, true); /// /// Returns the size relative to the source for the given transformation matrix. @@ -355,22 +357,22 @@ internal static class TransformUtils /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, false); + /// The to use when calculating the size. + /// The . + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, false); /// /// Returns the size relative to the source for the given transformation matrix. /// /// The transformation matrix. /// The source size. + /// The to use when calculating the size. /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -381,9 +383,13 @@ internal static class TransformUtils // Define an offset size to translate between coordinate space and pixel space. // Compute scaling factors from the matrix - float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) - float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) - SizeF offsetSize = new(scaleX, scaleY); + SizeF offsetSize = SizeF.Empty; + if (transformSpace == TransformSpace.Pixel) + { + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + offsetSize = new(scaleX, scaleY); + } // Subtract the offset size to translate to the pixel space. if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 06e6f0e71a..9027ee7266 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -13,6 +13,28 @@ public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public ProjectiveTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the projective transform. + /// + public ProjectiveTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the projective transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a matrix that performs a tapering projective transform. /// @@ -47,7 +69,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -81,7 +103,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -165,7 +187,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -203,7 +225,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. diff --git a/src/ImageSharp/Processing/TransformSpace.cs b/src/ImageSharp/Processing/TransformSpace.cs new file mode 100644 index 0000000000..bca676bd88 --- /dev/null +++ b/src/ImageSharp/Processing/TransformSpace.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing; + +/// +/// Represents the different spaces used in transformation operations. +/// +public enum TransformSpace +{ + /// + /// Coordinate space is a continuous, mathematical grid where objects and positions + /// are defined with precise, often fractional values. This space allows for fine-grained + /// transformations like scaling, rotation, and translation with high precision. + /// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries. + /// + Coordinate, + + /// + /// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen. + /// In this space, positions are defined by whole numbers, with no fractional values. + /// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3). + /// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen. + /// + Pixel +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 9d256efc1c..f046c2503c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Tests.Processing.Transforms; @@ -97,7 +98,7 @@ public abstract class TransformBuilderTestBase this.AppendRotationDegrees(builder, degrees); // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness - Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size); + Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -151,7 +152,7 @@ public abstract class TransformBuilderTestBase this.AppendSkewDegrees(builder, degreesX, degreesY); - Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size); + Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png index 2ba0560e65..480c07da48 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b926c8335eca5530d8704739cecae0799cc651139daedb1f88ac85b0ee1bd5d -size 9484 +oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729 +size 9747 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png index d01fcb4a49..227f546515 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:322c7e061f8565efdc19642e27353ec3073ee43d8c17fbef8c13be3bb60d11dc -size 10190 +oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db +size 10561 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png index 171080746b..b93742a858 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:544e7bac188d0869f98ec075fa0e73ab831e4dafe40c1520dce194df6a53c9b8 -size 12737 +oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0 +size 13227 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png index 07c65142a4..57c3b02ba7 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ccc08769974e4088702d2c95fd274af7e02095955953b424a6313d656a77735 -size 19974 +oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3 +size 20778 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png index 84fffa468f..b3bfc7ee51 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1179b300b35d526bab148833ab6240f1207b8ade36674b1f47cc5a2d47a084c -size 10603 +oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a +size 10943 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index 1f1d530517..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f666fe67ee4a1c7152fc6190affba95ea4cbd857d96bac0968e5f1fd89792d32 -size 13486 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 0ce7ad5625..63214687d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b05406a1d95f0709a7aaab7c1f57ba161b7907b76746f61788cfe527796a489 -size 4131 +oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735 +size 4165 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index e93934b48d..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be52d36cc8f616a781c8b1416ca0bf6207b9acd580e9c06e1ee5ad434d48ab38 -size 13481 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 2a68a381d4..ef37b3e2d6 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c99b8f0fb5d10a273a90946767f93ab6cd2dd1942f9829d695987db30dccfa -size 12488 +oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a +size 12559 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index 08f89da07e..93a0ce6c54 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af2c0201c59065a500ae985e9b7ca164e5bcb4ce2d8d8305103398830472e07c -size 14206 +oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2 +size 14223 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index 85d4871036..c2ca6bf57b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cef17988c4a3a667dede3dd86ed61d0507a84e5b846f52459683fd04e5a396a -size 17297 +oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272 +size 17927 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 347cbf089f..ade9a1ccd8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9699a81572c03c2bc47d8bbdd1d64df26f87df3d4ad59fb6f164f6e82786d91d -size 18853 +oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc +size 18945 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 69fe0b1355..cf04e20363 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fb1f59c5393debdff9bd4b7a6c222b7a0686e6d5ef24363e3d5c94ba9b5bc27 -size 20725 +oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af +size 20844 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 0fa8cf0c06..6be0fc0ff8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ffa8ca6a60de9fe26a191edc2127886c61c072c1aa2b91fe3125512fe40e1b3 -size 13848 +oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b +size 13857 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 36e409ecb9..0064e973ff 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abf1d0f323795c0aaff0ff8b488d9866c5b2f7c64aad83701cb1f60e22668b0e -size 4161 +oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f +size 4154 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index f8f102e4c8..5dd0c52255 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0f3c41248138bd501ae844e5b54fb9f49e5d22bab9b2ef0a0654034708b99f -size 14027 +oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07 +size 13991 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index fc46cad7c0..a6e120e904 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a3f839d64984b9fda4125c6643f4699add6f95373a2194c5726ed3740565a47 -size 13725 +oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b +size 13744 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 58e879d4e3..d32c11d44c 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ac143bc73612cecfffbec049a7b68234e7bf7581e680c3f996a977c6d671cc1 -size 14865 +oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca +size 14889 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index fa25146d9c..72782b0b99 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb9dab20d5a03c0adde05a9b741d9e1b0fb8c3d79054a8bc5788de496e5c7f8 -size 12420 +oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f +size 12374 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index dc8ea690c2..6cedab729b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f56ee78cc2fd698ac8ea84912648f0b49d4b4d66b439f6976447c56a44c2998 -size 16909 +oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328 +size 17027 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index 4b9953b670..7368a3b007 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd3b29b530e221618f65cd5e493b21fe3c27804fde7664636b7bb002f72abbb2 -size 3663 +oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921 +size 854 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 5f4911e47c..da66b26768 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a35757fef08a6fd9b37e719d5be7a82d5ff79f0395e082f697d9ebe9c7f03cc8 -size 5748 +oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a +size 5520 diff --git a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png index 17238cf2f7..7e693a5839 100644 --- a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png +++ b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00836a98742d177a2376af32d8d858fcf9f26a4da5a311dd5faf5cd80f233c0b -size 184397 +oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d +size 184124 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png index e7ed4a95f5..f2e87f8509 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d40d0715f695dc55dc2e435cab2c999b657b59ead2e0cc8a95edf7cea7782750 -size 6842 +oid sha256:455b17bc432490435c2453424d17b92b77d036dcbc2b2ab06b960a398bd3136b +size 11119 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 5518047e4d..f9aa1ffe03 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d502e2d6b0eb5f12ed3262eb4654927cc937574ae1de61a1d89f6672592017 -size 11828 +oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c +size 2757 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 29014117e3..2f9109ba38 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f1e69bfd1f4e9479839d4bddfb2ecc68ff8cda9b15055427406691df94a16db -size 9583 +oid sha256:57698b6666029a55edf8bd35a7ba96f68d224988cf01308a3af1c6606ae9d0b1 +size 10174 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index d417cad9c1..7dfec78983 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd1d0350ca49ff1726c2a20769693698168497944413c653da6edcb6bc9a39e5 -size 17344 +oid sha256:fc7c9da04142a679887c714c43f1838eba0092a869140d234fce3412673207c6 +size 13575 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index cfd3cd48ef..6e3b97f2df 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dc8ed5f721d2d8ae81f1b70308511737b0e649a6b05a011342efce29fa5b1cb -size 23022 +oid sha256:d8b973f41f8afa39b94c71b307b7eb393953e2d083d56e1f0e8f43d6ab1f342a +size 16821 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 2578c537e8..6986c03912 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9514b25bbc875eb00b5c0dc4241f973d466075914a9ec4c4b64ba251323eb5a -size 22321 +oid sha256:122c1501e09516244f0db36e1cca373ff68514a18e84f57ed3072d52d6112e36 +size 17022 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 1f6609c6f0..76b53fabfb 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09138a5ec45480a72370eba3015e7dee33360712745cb2f00e36a5994c1e48a7 -size 24090 +oid sha256:12181516bce69c9302f15bba928fd530796449599cb9744a2411cc796788ee3b +size 18066 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 7e4f16695a..ae4242a42b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed80155afab90710ebab4babbbb787402ceac4f9dee1ff270361e5e8e495c73a -size 13867 +oid sha256:1eb5accc5ada5b963ecef6ac15bfb1845f481e51aef63e06a522ea73bbeab945 +size 11194 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 3297f99e21..efb6a2deed 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96fa0ebee48290be808d5c7dec561bbcab49e5222667e175ffff01b3a175c586 -size 2308 +oid sha256:0418f0ea38ec19b407f2b5046d7ff0ed207189ad71db1e50e82c419d83620543 +size 2759 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index 48c51e7c3c..976be43a3b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c3ce34e6bbd8e35ec979acc28510342dadf0bae3d91a9e1b8291cdff638465 -size 13873 +oid sha256:1233a9ab2c4b0b17b0248c3d40050c466330c22095287dfbdb8bf7dfbda4ff1f +size 11212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index 379b25ca0e..04fb2e87e0 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87676e4bf55e71815acc46bdeb3384d659fe6cb3ecc7b730fd9ecc8be00d181f -size 13847 +oid sha256:e2912d4e42c7b76d9ff48a49921d6472e351662597d69b88bc3708683c7933e3 +size 11221 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 4bcd2dc604..b35d68aaf8 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34a62df6b4e9756a7dfef965b669b522e2338b0c2e267d35e4a37fcd9d8a55c3 -size 14818 +oid sha256:51b05c38647e0c1d88cc722e4109a882305073a065d2a27ccd3bee82f727127d +size 11775 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index 009fc4fa60..64b9c6aba4 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60cf2a3fae30bee16131092590e2a8fe139070b6cf98c3b98698e34ededb711f -size 11996 +oid sha256:b260e816b23a43d7efb7507897ba2f5dbb6a596dd67a5abd4c9a0c005e926ee0 +size 9748 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index 568625cd6c..29b95bf525 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2140365f533762cfc4411105be04a13f7d85739d955152c3e1951c2c69c7d67 -size 19934 +oid sha256:50b03d627bb53048f5e56380500f116da4c503f5bb6a1b1d3c0d67ee4256d8f6 +size 15977 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png index 92a99f0360..54dca26397 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57bd912acfd243882163775196e0b02c7d0374e81319cb29902cd560b5c6053 -size 394 +oid sha256:96454548849147d7c7f0ca507c8521a7d5eaa7771f9f383cc836858870b52c57 +size 280 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png index cc89f5114b..41f94c9c7b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8827b81929abd9000fcfd3ce747b82fec4c778bcd29bb12abaaed5f8b0dfc945 -size 228 +oid sha256:e94d224fdb284b6f1ba21b8caa66174edd7e6a3027f9dd03f4757e08296e6508 +size 212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png index 61128d7b83..49cd1c8375 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5819f0deb160a7c7bf090919a010a2f8a3c074b3b718f56fe56f1f6eae1fcdcd -size 227 +oid sha256:d1162be9fa1f31bee8d3cba05c1422a1945621a412be11cce13d376efd5c679c +size 173 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png index 846fe440ef..59f928178a 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1381429147add3a9d3342bb2d618a47a1a5db997a384582a288705f68f5f937a -size 343 +oid sha256:0ed262e9b885af773a4a40a4506e678630670e208bf7f9ec10307e943b166bed +size 258 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png index 7b0692ffe2..57ee3dc2ff 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e59892f86d307e28457ebd621fec84b8ab839cdc11afe38e978420c4173058e4 -size 236 +oid sha256:3a24f2cfc225d01294b8bbc5ca7d7f1738fb0b79217046eb9edf04e4c4c01851 +size 201 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png index 4c0d43feaf..7e47f43ff9 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d25a9407bb5a3c8ba7126c9082dfbc5f29c5b52fc46e1a21e1c8968ac9f1c11 -size 230 +oid sha256:938186fb3d0f468176988a9530efd22e66241a1361fff027005ec8a8ae323ff3 +size 197 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png index a832badcb0..0f756e7813 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:398d1ca71075c541fdb88eb63f5889edea2259fdd0df643bed77e64baa246ceb -size 317 +oid sha256:4bc4b8ea7e7f10676d8de612fe6bc5144e100b95ff3fe7a1e3d4066a7684ce4d +size 239 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png index bdef30001b..b2d420886b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62546cd019fa11a43d254936c12b5adcbe8eebe6ae012bc1024949df6b28303f -size 220 +oid sha256:345337f7dffa48d95251503ee2ae8e91db98b5cbe06b579d73c38a018c781544 +size 182 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png index 3012731317..4f0ad9d045 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e444aff3db9c5a1149de73f3b0ed18e48c8dc372be13c3555dcda0ec4ad893 -size 221 +oid sha256:de4e2b71dade9dfb750a2c614a684963d6962958db79145c87fd23d9f0f8c005 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png index dd6b926db4..78bdb8bbbe 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bdfa8eb65b5e54b9ccffb11acb2a3b7a5a434f1f22a2cdf792d892fd411f711 -size 399 +oid sha256:8d8b651663366e7543211635f337c229e2f88f1142886ea3a9b69587daaada97 +size 288 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png index fb98036ec4..7015a05571 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11a1af248350450454ba1cb415357e58f905de402a921b213a305990e8f57c3 -size 245 +oid sha256:8ab8df31f1716c05bb8687f79c7d1154f6cc6f65e3917abe60ecc42d0df173dc +size 215 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png index 0e4ef4c427..67a765e8db 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9cffb16fbff28901a3daf391469d32bf36f3c293aa870d169967f17611dea92 -size 241 +oid sha256:1a1671da9ea7702a37a866fabfb3ca0d746233ee108594198f23cb563af43ae6 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index e8efa8a980..9d7dc06c95 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39c25539c3c9b8926bf65c041df693a60617bbe8653bb72357bde5ab6342c59c -size 3618 +oid sha256:f5aef9cd3b8bfd9859e5d2401e82f89d89407ab2834b09c43f0a3229c735e92b +size 724 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 99a74e400a..4b2bb99d96 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b1fc95fdf07c7443147205afffb157aa82f94818cfbb833a615c42f584fbda0 -size 5070 +oid sha256:1e7dedec16ccd66543a1d44052a104957ba62099ba2f2ccc72285c233c2ae3fa +size 4411 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png index a9289abd0d..65bb77977b 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801067dfb19b2a9a1fbd14b771e780b475c21f3ccdc1e709bbc20d62061ad1d1 -size 8782 +oid sha256:3594547265b23603b1a76ff6bc6f0eab4af55d6e0070e53356123dfc7ae256f8 +size 9034 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png index 43fcd5df5e..7c54b1b074 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccbdd3dcdbc888923e85495fbd7837478cca813be6ecece63ee645bdf39d436f -size 10325 +oid sha256:5ae9ef073f3338b71d2a40fcf2e89d9b6ab62204d6de9b6a1f75f4705ee197f0 +size 10704 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png index 9a7f9866da..b6e930224e 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60d1be2ffa6d50f97561b92efe8d0c0337f9c121582e38c9ab9af75be8eed32d -size 8539 +oid sha256:994dda7da034595aa77d107652bea06c86077d24ef8a6883b18f1f509bb19928 +size 8906 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png index 1d27e23958..2f3f0f17fe 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75fb59ea2947efb1abf73cd824e54b6e8f6271343bd2fdadb005b29476988921 -size 9423 +oid sha256:29c5f48f1ece0b12854b4c44fba84fdfc9ac5751cdf564a32478dcdaed43b2a4 +size 9798 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png index 628d0c889c..5242a9d985 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2d94791af40e001fc44b23eeecac7a606492c747b22ede0cdc7069ef121cb8 -size 11193 +oid sha256:c7de58474c3f386c4ec31a9088d561a513f82c08d1157132d735169b847b9680 +size 11579 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png index 0e927cfbd0..2af9d2fc27 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:016de6e82b6fb03fd55168ea7fc12ab245d0e0387ca7c32d3ef1158a85a8facd -size 9330 +oid sha256:3ef9b7051d7a5733dfe2534fddefdc28dfbc49d087355f46c4d945b04f0e3936 +size 9672 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png index ac4d473624..83c02764fa 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9219cc118fe7195b730cbe2e6407cde54e6f4c7930a71b7418bc7c273aa4120c -size 11050 +oid sha256:825770c9b2e9f265d834eab6b40604df5508bf9bc5b4f82f5d3effd6d5a26935 +size 11434 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png index 147f9c9897..d6dba3f889 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b243340f372220033b349a96cbd7ecd732395fa15e4d1ed62048d2031c42794 -size 8398 +oid sha256:1e283463b0f450dd72cf303acccf3dd1ff7a31fe401ff0f288d67c4baefca240 +size 8742 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png index d1252cb2c4..76bb244d52 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd2745aba2f09bb4a09e881339fe62774ce5658902aa9d83b3a1e0718260084 -size 8694 +oid sha256:485d9d9ef955a04af43d17e6bc3952e9bf65a9752b6cf8ba9cbbe8f772f05a18 +size 8995 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png index 235e95d8fc..c1c1d814fd 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281bd00550ab1cf9b05011750e6de330cdd42a8644ecf3b7c176bd5c6e94c59b -size 6098 +oid sha256:d3d749ac365764051ea16bc39d1aff84c06faf282359805b58bb97c9eed7f0bb +size 6400 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png index 3b63f58f36..27608881ed 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e755ed61d7eab2c297f4b6592366b54ac801cbdb3d920741dfdd04dfaf73f9b9 -size 6086 +oid sha256:8d82f2a15502b0a29aa4df1077ec90c88f9211f283fdc0edd7b059ed9b387441 +size 6334