diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs index a2dd5d9ced..4d94f583ab 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieLabCieLab +/// +/// Allows conversion between two color profiles based on the CIE Lab color space. +/// +public static class ColorProfileConverterExtensionsCieLabCieLab { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsCieLabCieLab return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs index 096622564c..1de4510bc9 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieLabCieXyz +/// +/// Allows conversion between two color profiles based on the CIE Lab and CIE XYZ color spaces. +/// +public static class ColorProfileConverterExtensionsCieLabCieXyz { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs index 51be13799c..4f0d470806 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieLabRgb +/// +/// Allows conversion between two color profiles based on the CIE Lab and RGB color spaces. +/// +public static class ColorProfileConverterExtensionsCieLabRgb { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsCieLabRgb return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs index 3bab4e7b16..3bb1b2d4f8 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieXyzCieLab +/// +/// Allows conversion between two color profiles based on the CIE XYZ and CIE Lab color spaces. +/// +public static class ColorProfileConverterExtensionsCieXyzCieLab { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs index 5188511476..dabca45793 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieXyzCieXyz +/// +/// Allows conversion between two color profiles based on the CIE XYZ color space. +/// +public static class ColorProfileConverterExtensionsCieXyzCieXyz { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -30,6 +46,20 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz return TTo.FromProfileConnectingSpace(options, in pcsFrom); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs index c56bf214b9..1803c0839c 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsCieXyzRgb +/// +/// Allows conversion between two color profiles based on the CIE XYZ and RGB color spaces. +/// +public static class ColorProfileConverterExtensionsCieXyzRgb { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieXyzRgb return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index c33f40001a..3ddbf93b58 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -60,8 +60,8 @@ internal static class ColorProfileConverterExtensionsIcc ColorProfileConverter pcsConverter = new(new ColorConversionOptions { MemoryAllocator = converter.Options.MemoryAllocator, - SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), - TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), + SourceWhitePoint = KnownIlluminants.D50Icc, + TargetWhitePoint = KnownIlluminants.D50Icc }); // Normalize the source, then convert to the PCS space. @@ -104,8 +104,8 @@ internal static class ColorProfileConverterExtensionsIcc ColorProfileConverter pcsConverter = new(new ColorConversionOptions { MemoryAllocator = converter.Options.MemoryAllocator, - SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), - TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), + SourceWhitePoint = KnownIlluminants.D50Icc, + TargetWhitePoint = KnownIlluminants.D50Icc }); using IMemoryOwner pcsBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs index badbcc6831..c2ed9a5918 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsRgbCieLab +/// +/// Allows conversion between two color profiles based on the RGB and CIE Lab color spaces. +/// +public static class ColorProfileConverterExtensionsRgbCieLab { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsRgbCieLab return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs index cd7d5e4d65..9cf7ec70d9 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsRgbCieXyz +/// +/// Allows conversion between two color profiles based on the RGB and CIE XYZ color spaces. +/// +public static class ColorProfileConverterExtensionsRgbCieXyz { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsRgbCieXyz return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs index 2a4b64b1ca..34f3f7f191 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs @@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; -internal static class ColorProfileConverterExtensionsRgbRgb +/// +/// Allows conversion between two color profiles based on the RGB color space. +/// +public static class ColorProfileConverterExtensionsRgbRgb { + /// + /// Converts a color value from one color profile to another using the specified color profile converter. + /// + /// + /// The conversion process may use ICC profiles if available; otherwise, it performs a manual + /// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires + /// both source and target types to be value types implementing the appropriate color profile interface. + /// + /// The source color profile type. Must implement . + /// The target color profile type. Must implement . + /// The color profile converter to use for the conversion. + /// The source color value to convert. + /// A value of type representing the converted color in the target color profile. public static TTo Convert(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsRgbRgb return TTo.FromProfileConnectingSpace(options, in pcsTo); } + /// + /// Converts a span of color values from one color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion between two color profiles, handling necessary + /// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are + /// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory + /// for the destination; the caller is responsible for providing a suitably sized span. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter to use for the conversion operation. + /// A read-only span containing the source color values to convert. + /// A span that receives the converted color values. Must be at least as long as the source span. public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/KnownIlluminants.cs b/src/ImageSharp/ColorProfiles/KnownIlluminants.cs index b9236497fe..20ba445ecc 100644 --- a/src/ImageSharp/ColorProfiles/KnownIlluminants.cs +++ b/src/ImageSharp/ColorProfiles/KnownIlluminants.cs @@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.ColorProfiles; /// /// /// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html +/// and https://color.org/specification/ICC.1-2022-05.pdf ///
/// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant ///
@@ -30,10 +31,15 @@ public static class KnownIlluminants public static CieXyz C { get; } = new(0.98074F, 1F, 1.18232F); /// - /// Gets the Horizon Light. ICC profile PCS illuminant. + /// Gets the Horizon Light. /// public static CieXyz D50 { get; } = new(0.96422F, 1F, 0.82521F); + /// + /// Gets the D50 illuminant used in the ICC profile specification. + /// + public static CieXyz D50Icc { get; } = new(0.9642F, 1F, 0.8249F); + /// /// Gets the Mid-morning / Mid-afternoon Daylight illuminant. /// diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index 2511cffdbb..bb6c2a2825 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -78,7 +78,7 @@ public sealed class DecoderOptions return false; } - if (IccProfileHeader.IsLikelySrgb(profile.Header)) + if (profile.IsCanonicalSrgbMatrixTrc()) { return false; } @@ -99,7 +99,7 @@ public sealed class DecoderOptions return false; } - if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header)) + if (this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc()) { return true; } diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index 701b9af053..7e0b56beb3 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -32,7 +32,7 @@ public class PngFrameMetadata : IFormatFrameMetadata /// /// Gets or sets the frame delay for animated images. - /// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to + /// If not 0, when utilized in Png animation, this field specifies the number of seconds to /// wait before continuing with the processing of the Data Stream. /// The clock starts ticking immediately after the graphic is rendered. /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index 03cd639ad3..86b5c19d21 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -28,20 +28,20 @@ internal abstract class TiffBaseDecompressor : TiffBaseCompression /// Decompresses image data into the supplied buffer. /// /// The to read image data from. - /// The strip offset of stream. - /// The number of bytes to read from the input stream. + /// The data offset within the stream. + /// The number of bytes to read from the input stream. /// The height of the strip. /// The output buffer for uncompressed data. /// The token to monitor cancellation. - public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + public void Decompress(BufferedReadStream stream, ulong offset, ulong count, int stripHeight, Span buffer, CancellationToken cancellationToken) { - DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset)); - DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount)); + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)long.MaxValue, nameof(offset)); + DebugGuard.MustBeLessThanOrEqualTo(count, (ulong)int.MaxValue, nameof(count)); - stream.Seek((long)stripOffset, SeekOrigin.Begin); - this.Decompress(stream, (int)stripByteCount, stripHeight, buffer, cancellationToken); + stream.Seek((long)offset, SeekOrigin.Begin); + this.Decompress(stream, (int)count, stripHeight, buffer, cancellationToken); - if ((long)stripOffset + (long)stripByteCount < stream.Position) + if ((long)offset + (long)count < stream.Position) { TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index fbff352970..8a4a27946f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; @@ -441,8 +442,14 @@ internal class TiffDecoderCore : ImageDecoderCore { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); - stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize); } using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); @@ -507,15 +514,83 @@ internal class TiffDecoderCore : ImageDecoderCore rowsPerStrip = height; } - int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); + ulong 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(width, bitsPerPixel, frame.Metadata); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); + Buffer2D pixels = frame.PixelBuffer; + + // There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue. + // We can read them, but we cannot allocate a buffer that large to hold the uncompressed data. + // In this scenario we fall back to reading and decoding one row at a time. + // + // The NoneTiffCompression decompressor can be used to read individual rows since we have + // a guarantee that each row required the same number of bytes. + if (decompressor is NoneTiffCompression none && uncompressedStripSize > int.MaxValue) + { + ulong bytesPerRowU = this.CalculateStripBufferSize(width, 1); + + // This should never happen, but we check just to be sure. + if (bytesPerRowU > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + int bytesPerRow = (int)bytesPerRowU; + using IMemoryOwner rowBufferOwner = this.memoryAllocator.Allocate(bytesPerRow, AllocationOptions.Clean); + Span rowBuffer = rowBufferOwner.GetSpan(); + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 + ? rowsPerStrip + : height % rowsPerStrip; + + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > height) + { + break; + } + + ulong baseOffset = stripOffsets[stripIndex]; + ulong available = stripByteCounts[stripIndex]; + ulong required = (ulong)bytesPerRow * (ulong)stripHeight; + if (available < required) + { + break; + } + + for (int r = 0; r < stripHeight; r++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ulong rowOffset = baseOffset + ((ulong)r * (ulong)bytesPerRow); + + // Use the NoneTiffCompression decompressor to read exactly one row. + none.Decompress( + this.inputStream, + rowOffset, + (ulong)bytesPerRow, + 1, + rowBuffer, + cancellationToken); + + colorDecoder.Decode(rowBuffer, pixels, 0, top + r, width, 1); + } + } + + return; + } + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate((int)uncompressedStripSize, AllocationOptions.Clean); + Span stripBufferSpan = stripBuffer.GetSpan(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { @@ -808,7 +883,7 @@ internal class TiffDecoderCore : ImageDecoderCore /// The height for the desired pixel buffer. /// The index of the plane for planar image configuration (or zero for chunky). /// The size (in bytes) of the required pixel buffer. - private int CalculateStripBufferSize(int width, int height, int plane = -1) + private ulong CalculateStripBufferSize(int width, int height, int plane = -1) { DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); @@ -841,8 +916,8 @@ internal class TiffDecoderCore : ImageDecoderCore } } - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - return bytesPerRow * height; + ulong bytesPerRow = (((ulong)width * (ulong)bitsPerPixel) + 7) / 8; + return bytesPerRow * (ulong)height; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 173d9436dd..86489cd363 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -32,6 +32,11 @@ internal class WebpAnimationDecoder : IDisposable /// private readonly uint maxFrames; + /// + /// Whether to skip metadata. + /// + private readonly bool skipMetadata; + /// /// The area to restore. /// @@ -57,19 +62,97 @@ internal class WebpAnimationDecoder : IDisposable /// private readonly BackgroundColorHandling backgroundColorHandling; + /// + /// How to handle validation of errors in different segments of encoded image files. + /// + private readonly SegmentIntegrityHandling segmentIntegrityHandling; + /// /// Initializes a new instance of the class. /// /// The memory allocator. /// The global configuration. /// The maximum number of frames to decode. Inclusive. + /// Whether to skip metadata. /// The flag to decide how to handle the background color in the Animation Chunk. - public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling) + /// How to handle validation of errors in different segments of encoded image files. + public WebpAnimationDecoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + uint maxFrames, + bool skipMetadata, + BackgroundColorHandling backgroundColorHandling, + SegmentIntegrityHandling segmentIntegrityHandling) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.maxFrames = maxFrames; + this.skipMetadata = skipMetadata; this.backgroundColorHandling = backgroundColorHandling; + this.segmentIntegrityHandling = segmentIntegrityHandling; + } + + /// + /// Reads the animated webp image information from the specified stream. + /// + /// The stream, where the image should be decoded from. Cannot be null. + /// The webp features. + /// The width of the image. + /// The height of the image. + /// The size of the image data in bytes. + public ImageInfo Identify( + BufferedReadStream stream, + WebpFeatures features, + uint width, + uint height, + uint completeDataSize) + { + List framesMetadata = []; + this.metadata = new ImageMetadata(); + this.webpMetadata = this.metadata.GetWebpMetadata(); + this.webpMetadata.RepeatCount = features.AnimationLoopCount; + + Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? Color.Transparent + : features.AnimationBackgroundColor!.Value; + + this.webpMetadata.BackgroundColor = backgroundColor; + + Span buffer = stackalloc byte[4]; + uint frameCount = 0; + int remainingBytes = (int)completeDataSize; + while (remainingBytes > 0) + { + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + remainingBytes -= 4; + switch (chunkType) + { + case WebpChunkType.FrameData: + + ImageFrameMetadata frameMetadata = new(); + uint dataSize = ReadFrameInfo(stream, ref frameMetadata); + framesMetadata.Add(frameMetadata); + + remainingBytes -= (int)dataSize; + break; + case WebpChunkType.Xmp: + case WebpChunkType.Exif: + WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer); + break; + default: + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. + break; + } + + if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + { + break; + } + } + + return new ImageInfo(new Size((int)width, (int)height), this.metadata, framesMetadata); } /// @@ -128,10 +211,12 @@ internal class WebpAnimationDecoder : IDisposable break; case WebpChunkType.Xmp: case WebpChunkType.Exif: - WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer); + WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer); break; default: - WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data"); + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. break; } @@ -144,6 +229,26 @@ internal class WebpAnimationDecoder : IDisposable return image!; } + /// + /// Reads frame information from the specified stream and updates the provided frame metadata. + /// + /// The stream from which to read the frame information. Must support reading and seeking. + /// A reference to the structure that will be updated with the parsed frame metadata. + /// The number of bytes read from the stream while parsing the frame information. + private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata) + { + WebpFrameData frameData = WebpFrameData.Parse(stream); + SetFrameMetadata(frameMetadata, frameData); + + // Size of the frame header chunk. + const int chunkHeaderSize = 16; + + uint remaining = frameData.DataSize - chunkHeaderSize; + stream.Skip((int)remaining); + + return remaining; + } + /// /// Reads an individual webp frame. /// @@ -155,6 +260,7 @@ internal class WebpAnimationDecoder : IDisposable /// The width of the image. /// The height of the image. /// The default background color of the canvas in. + /// The number of bytes read from the stream while parsing the frame information. private uint ReadFrame( BufferedReadStream stream, ref Image? image, diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 8df159dbff..dc95ca0443 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -120,6 +121,7 @@ internal static class WebpChunkParsingUtils return new WebpImageInfo { + DataSize = dataSize, Width = width, Height = height, XScale = xScale, @@ -178,6 +180,7 @@ internal static class WebpChunkParsingUtils return new WebpImageInfo { + DataSize = imageDataSize, Width = width, Height = height, BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, @@ -333,7 +336,13 @@ internal static class WebpChunkParsingUtils /// If there are more such chunks, readers MAY ignore all except the first one. /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. /// - public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, Span buffer) + public static void ParseOptionalChunks( + BufferedReadStream stream, + WebpChunkType chunkType, + ImageMetadata metadata, + bool ignoreMetaData, + SegmentIntegrityHandling segmentIntegrityHandling, + Span buffer) { long streamLength = stream.Length; while (stream.Position < streamLength) @@ -353,12 +362,30 @@ internal static class WebpChunkParsingUtils bytesRead = stream.Read(exifData, 0, (int)chunkLength); if (bytesRead != chunkLength) { - WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); + if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone) + { + WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); + } + + return; } - if (metadata.ExifProfile != null) + if (metadata.ExifProfile == null) { - metadata.ExifProfile = new ExifProfile(exifData); + ExifProfile exifProfile = new(exifData); + + // Set the resolution from the metadata. + double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution); + double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution); + + if (horizontalValue > 0 && verticalValue > 0) + { + metadata.HorizontalResolution = horizontalValue; + metadata.VerticalResolution = verticalValue; + metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile); + } + + metadata.ExifProfile = exifProfile; } break; @@ -367,14 +394,16 @@ internal static class WebpChunkParsingUtils bytesRead = stream.Read(xmpData, 0, (int)chunkLength); if (bytesRead != chunkLength) { - WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); - } + if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone) + { + WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); + } - if (metadata.XmpProfile != null) - { - metadata.XmpProfile = new XmpProfile(xmpData); + return; } + metadata.XmpProfile ??= new XmpProfile(xmpData); + break; default: stream.Skip((int)chunkLength); @@ -383,6 +412,16 @@ internal static class WebpChunkParsingUtils } } + private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag) + { + if (exifProfile.TryGetValue(tag, out IExifValue? resolution)) + { + return resolution.Value.ToDouble(); + } + + return 0; + } + /// /// Determines if the chunk type is an optional VP8X chunk. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 51379a32ae..0e9888adb2 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -56,6 +56,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable /// private readonly BackgroundColorHandling backgroundColorHandling; + private readonly SegmentIntegrityHandling segmentIntegrityHandling; + /// /// Initializes a new instance of the class. /// @@ -64,6 +66,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable : base(options.GeneralOptions) { this.backgroundColorHandling = options.BackgroundColorHandling; + this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; this.maxFrames = options.GeneralOptions.MaxFrames; @@ -89,7 +92,10 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.memoryAllocator, this.configuration, this.maxFrames, - this.backgroundColorHandling); + this.skipMetadata, + this.backgroundColorHandling, + this.segmentIntegrityHandling); + return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -101,6 +107,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + losslessDecoder.Decode(pixels, image.Width, image.Height); } else @@ -109,6 +116,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -131,11 +139,29 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable /// protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - ReadImageHeader(stream, stackalloc byte[4]); - + uint fileSize = ReadImageHeader(stream, stackalloc byte[4]); ImageMetadata metadata = new(); + using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { + if (this.webImageInfo.Features is { Animation: true }) + { + using WebpAnimationDecoder animationDecoder = new( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.skipMetadata, + this.backgroundColorHandling, + this.segmentIntegrityHandling); + + return animationDecoder.Identify( + stream, + this.webImageInfo.Features, + this.webImageInfo.Width, + this.webImageInfo.Height, + fileSize); + } + return new ImageInfo( new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), metadata); @@ -211,6 +237,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { + // ANIM chunks appear before EXIF and XMP chunks. + // Return after parsing an ANIM chunk - The animated decoder will handle the rest. bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { @@ -273,7 +301,9 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.ReadAlphaData(stream, features, ignoreAlpha, buffer); break; default: - WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. break; } @@ -335,7 +365,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize); if (bytesRead != exifChunkSize) { - // Ignore invalid chunk. + if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone) + { + WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); + } + return; } @@ -385,7 +419,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); if (bytesRead != xmpChunkSize) { - // Ignore invalid chunk. + if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone) + { + WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); + } + return; } diff --git a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs index 3428ce199a..e0993145f0 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Webp; internal class WebpImageInfo : IDisposable { + /// + /// Gets or sets the size of the encoded image data in bytes. + /// + public uint DataSize { get; set; } + /// /// Gets or sets the bitmap width in pixels. /// diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs new file mode 100644 index 0000000000..bfa4ab9bdb --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs @@ -0,0 +1,346 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; + +/// +/// Provides logic for identifying canonical IEC 61966-2-1 (sRGB) matrix-TRC ICC profiles, +/// distinguishing them from appearance or device-specific variants. +/// +public sealed partial class IccProfile +{ + // sRGB v2 Preference + private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D); + + /// + /// Detects canonical sRGB matrix+TRC profiles quickly and safely. + /// Rules: + /// 1) Accept known IEC sRGB v2 and v4 by profile ID. + /// 2) Require RGB, PCS=XYZ, ICC v2 or v4, and no A2B*/B2A* LUTs. + /// 3) Require rTRC, gTRC, bTRC to exist and be identical by parameters or sampled shape. + /// 4) Accept if rXYZ/gXYZ/bXYZ already match the D50-adapted sRGB colorants within tolerance. + /// 5) If white point ≈ D65, adapt only the colorant columns to D50 using Bradford + /// via and then compare. + /// This rejects channel-swapped and appearance profiles while allowing real sRGB. + /// + /// + /// Reference D50-adapted sRGB colorants from Bruce Lindbloom: + /// + /// R=(0.4360747, 0.2225045, 0.0139322) + /// G=(0.3850649, 0.7168786, 0.0971045) + /// B=(0.1430804, 0.0606169, 0.7141733) + /// + internal bool IsCanonicalSrgbMatrixTrc() + { + IccProfileHeader h = this.Header; + + // Fast path for known IEC sRGB profile IDs + if (h.Id == StandardRgbV2 || h.Id == StandardRgbV4) + { + return true; + } + + // Header gating to avoid parsing work for obvious non-matches + if (h.FileSignature != "acsp") + { + return false; + } + + if (h.DataColorSpace != IccColorSpaceType.Rgb) + { + return false; + } + + if (h.ProfileConnectionSpace != IccColorSpaceType.CieXyz) + { + return false; + } + + if (h.Version.Major is not 2 and not 4) + { + return false; + } + + this.InitializeEntries(); + IccTagDataEntry[] entries = this.entries; + + // Reject device/display LUT profiles. We only accept matrix+TRC encodings. + if (Has(entries, IccProfileTag.AToB0) || Has(entries, IccProfileTag.AToB1) || Has(entries, IccProfileTag.AToB2) || + Has(entries, IccProfileTag.BToA0) || Has(entries, IccProfileTag.BToA1) || Has(entries, IccProfileTag.BToA2)) + { + return false; + } + + // Required matrix+TRC tags + if (!TryGetXyz(entries, IccProfileTag.MediaWhitePoint, out Vector3 wtpt)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.RedMatrixColumn, out Vector3 rXYZ)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.GreenMatrixColumn, out Vector3 gXYZ)) + { + return false; + } + + if (!TryGetXyz(entries, IccProfileTag.BlueMatrixColumn, out Vector3 bXYZ)) + { + return false; + } + + // TRCs must exist and be identical across channels. This filters many trick profiles. + if (!TryGetTrc(entries, IccProfileTag.RedTrc, out Trc tR)) + { + return false; + } + + if (!TryGetTrc(entries, IccProfileTag.GreenTrc, out Trc tG)) + { + return false; + } + + if (!TryGetTrc(entries, IccProfileTag.BlueTrc, out Trc tB)) + { + return false; + } + + if (!tR.Equals(tG) || !tR.Equals(tB)) + { + return false; + } + + // D50-adapted sRGB colorants (compare as columns: r,g,b), tight epsilon + const float eps = 2e-3F; + Vector3 rRef = new(0.4360747F, 0.2225045F, 0.0139322F); + Vector3 gRef = new(0.3850649F, 0.7168786F, 0.0971045F); + Vector3 bRef = new(0.1430804F, 0.0606169F, 0.7141733F); + + // First, accept if the stored colorants are already the D50 sRGB primaries. + // Many v2 sRGB profiles store D50-adapted colorants while declaring wtpt≈D65. + if (Near(rXYZ, rRef, eps) && Near(gXYZ, gRef, eps) && Near(bXYZ, bRef, eps)) + { + return true; + } + + // If the profile declares a D65 white, adapt the colorant columns to D50 and compare again. + // We never adapt when they already match, to avoid compounding rounding. + if (Near(wtpt, KnownIlluminants.D65.AsVector3Unsafe(), 2e-3F)) + { + CieXyz fromWp = new(wtpt); // Declared white + CieXyz toWp = KnownIlluminants.D50; // PCS white + Matrix4x4 matrix = KnownChromaticAdaptationMatrices.Bradford; + + rXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(rXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + gXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(gXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + bXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(bXYZ), (fromWp, toWp), matrix).AsVector3Unsafe(); + } + + // Require identity mapping of primaries, no permutation + if (!Near(rXYZ, rRef, eps) || !Near(gXYZ, gRef, eps) || !Near(bXYZ, bRef, eps)) + { + return false; + } + + return true; + + static bool Has(ReadOnlySpan span, IccProfileTag tag) + { + for (int i = 0; i < span.Length; i++) + { + if (span[i].TagSignature == tag) + { + return true; + } + } + + return false; + } + + static bool TryGetXyz(ReadOnlySpan span, IccProfileTag tag, out Vector3 xyz) + { + for (int i = 0; i < span.Length; i++) + { + IccTagDataEntry e = span[i]; + if (e.TagSignature != tag) + { + continue; + } + + if (e is IccXyzTagDataEntry x && x.Data is { Length: >= 1 }) + { + xyz = x.Data[0]; + return true; + } + + break; + } + + xyz = default; + return false; + } + + static bool TryGetTrc(ReadOnlySpan span, IccProfileTag tag, out Trc trc) + { + for (int i = 0; i < span.Length; i++) + { + IccTagDataEntry e = span[i]; + if (e.TagSignature != tag) + { + continue; + } + + if (e is IccParametricCurveTagDataEntry p) + { + trc = Trc.FromParametric(p.Curve); + return true; + } + + if (e is IccCurveTagDataEntry c) + { + trc = Trc.FromCurveLut(c.CurveData); + return true; + } + + break; + } + + trc = default; + return false; + } + + static bool Near(in Vector3 a, in Vector3 b, float tol) + => MathF.Abs(a.X - b.X) <= tol && + MathF.Abs(a.Y - b.Y) <= tol && + MathF.Abs(a.Z - b.Z) <= tol; + } + + /// + /// Compact, allocation-free descriptor of a TRC for equality and optional sRGB check. + /// + private readonly struct Trc : IEquatable + { + private readonly byte kind; // 0 = none, 1 = parametric, 2 = sampled + private readonly float g; // parametric payload or downsampled hash + private readonly float a; + private readonly float b; + private readonly float c; + private readonly float d; + private readonly float e; + private readonly float f; + private readonly int n; // for sampled, length or a small signature + + private Trc(byte kind, float g, float a, float b, float c, float d, float e, float f, int n) + { + this.kind = kind; + this.g = g; + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + this.n = n; + } + + public static Trc FromParametric(IccParametricCurve c) + + // Normalize by curve type to a stable tuple + // The types map to piecewise forms, but equality across channels is the key requirement here + => new(1, c.G, c.A, c.B, c.C, c.D, c.E, c.F, (int)c.Type); + + public static Trc FromCurveLut(float[] data) + { + // Exact sequence equality is enforced by the calling code using the same Trc construction + // Record a short signature to compare cheaply, avoid copying + if (data == null) + { + return default; + } + + int n = data.Length; + if (n == 0) + { + return default; + } + + // Downsample a few points to a robust fingerprint + // Use fixed indices to avoid allocations + float s0 = data[0]; + float s1 = data[n >> 2]; + float s2 = data[n >> 1]; + float s3 = data[(n * 3) >> 2]; + float s4 = data[n - 1]; + + return new Trc( + 2, + s0, + s1, + s2, + s3, + s4, + 0F, + 0F, + n); + } + + public override bool Equals(object? obj) => obj is Trc trc && this.Equals(trc); + + public bool Equals(Trc other) + { + if (this.kind != other.kind) + { + return false; + } + + if (this.kind == 0) + { + return false; + } + + if (this.kind == 1) + { + // parametric: exact parameter match and type match + return this.n == other.n && + this.g == other.g && this.a == other.a && + this.b == other.b && this.c == other.c && + this.d == other.d && this.e == other.e && this.f == other.f; + } + + // sampled: same length and same 5-point fingerprint + return this.n == other.n && + this.g == other.g && this.a == other.a && + this.b == other.b && this.c == other.c && this.d == other.d; + } + + // Optional stricter sRGB check if you need it later + public bool IsSrgbLike() + { + if (this.kind == 1) + { + // Accept common sRGB parametric encodings where type and parameters match + // IEC 61966-2-1 maps to Type4 or Type5 forms in practice + // Tighten only if you must exclude gamma~2.2 profiles that share primaries + return true; + } + + return true; + } + + public override int GetHashCode() + { + int a = HashCode.Combine(this.kind, this.g, this.a, this.b, this.c, this.d, this.e); + int b = HashCode.Combine(this.f, this.n); + return HashCode.Combine(a, b); + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 392ccb3062..05be3eb5dd 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; /// /// Represents an ICC profile /// -public sealed class IccProfile : IDeepCloneable +public sealed partial class IccProfile : IDeepCloneable { /// /// The byte array to read the ICC profile from diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs index b50885d025..959668aaf9 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs @@ -11,17 +11,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; /// public sealed class IccProfileHeader { - private static readonly Vector3 TruncatedD50 = new(0.9642029F, 1F, 0.8249054F); - - // sRGB v2 Preference - private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); - - // sRGB v4 Preference - private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D); - - // sRGB v4 Appearance - private static readonly IccProfileId StandardRgbV4A = new(0xDF1132A1, 0x746E97B0, 0xAD85719, 0xBE711E08); - /// /// Gets or sets the profile size in bytes (will be ignored when writing a profile). /// @@ -108,31 +97,4 @@ public sealed class IccProfileHeader /// Gets or sets the profile ID (hash). /// public IccProfileId Id { get; set; } - - internal static bool IsLikelySrgb(IccProfileHeader header) - { - // Reject known perceptual-appearance profile - // This profile employs perceptual rendering intents to maintain color appearance across different - // devices and media, which can lead to variations from standard sRGB representations. - if (header.Id == StandardRgbV4A) - { - return false; - } - - // Accept known sRGB profile IDs - if (header.Id == StandardRgbV2 || header.Id == StandardRgbV4) - { - return true; - } - - // Fallback: best-guess heuristic - return - header.FileSignature == "acsp" && - header.DataColorSpace == IccColorSpaceType.Rgb && - (header.ProfileConnectionSpace == IccColorSpaceType.CieXyz || header.ProfileConnectionSpace == IccColorSpaceType.CieLab) && - (header.Class == IccProfileClass.DisplayDevice || header.Class == IccProfileClass.ColorSpace) && - header.PcsIlluminant == TruncatedD50 && - (header.Version.Major == 2 || header.Version.Major == 4) && - !string.Equals(header.CmmType, "ADBE", StringComparison.Ordinal); - } } diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index 6c56dc682d..cb349af96a 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs @@ -42,7 +42,7 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) [InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, 0.0005)] // CMYK -> LAB -> XYZ -> RGB (different LUT tags, A2B vs TRC) --- tolerance slightly higher due to difference in inverse curve implementation [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) - public void CanConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005) + public void CanConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.000005) { List actual = Inputs.ConvertAll(input => GetActualTargetValues(input, sourceProfile, targetProfile)); AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper); @@ -63,7 +63,7 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, 0.0005)] // CMYK -> LAB -> XYZ -> RGB (different LUT tags, A2B vs TRC) --- tolerance slightly higher due to difference in inverse curve implementation [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) [InlineData(TestIccProfiles.Issue129, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> -> XYZ -> RGB - public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005) + public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.000005) { List actual = GetBulkActualTargetValues(Inputs, sourceProfile, targetProfile); AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 2856abe5c1..71753bf9ca 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -402,6 +402,9 @@ public partial class JpegDecoderTests [WithFile(TestImages.Jpeg.ICC.ProPhoto, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.ICC.WideRGB, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.ICC.AppleRGB, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.SRgbGray, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.Perceptual, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.PerceptualcLUTOnly, PixelTypes.Rgba32)] public void Decode_RGB_ICC_Jpeg(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 111544f7f5..c0abed214b 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -314,6 +314,21 @@ public class WebpDecoderTests Assert.Equal(12, image.Frames.Count); } + [Theory] + [InlineData(Lossless.Animated)] + public void Info_AnimatedLossless_VerifyAllFrames(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream); + WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata(); + + Assert.Equal(0, webpMetaData.RepeatCount); + Assert.Equal(150U, frameMetaData.FrameDelay); + Assert.Equal(12, image.FrameCount); + } + [Theory] [WithFile(Lossy.Animated, PixelTypes.Rgba32)] public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider provider) @@ -331,6 +346,21 @@ public class WebpDecoderTests Assert.Equal(12, image.Frames.Count); } + [Theory] + [InlineData(Lossy.Animated)] + public void Info_AnimatedLossy_VerifyAllFrames(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream); + WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata(); + + Assert.Equal(0, webpMetaData.RepeatCount); + Assert.Equal(150U, frameMetaData.FrameDelay); + Assert.Equal(12, image.FrameCount); + } + [Theory] [WithFile(Lossless.Animated, PixelTypes.Rgba32)] public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 3e5b3b7120..af6148c873 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -216,6 +216,9 @@ public static class TestImages public const string AppleRGB = "Jpg/icc-profiles/Momiji-AppleRGB-yes.jpg"; public const string CMYK = "Jpg/icc-profiles/issue-129.jpg"; public const string YCCK = "Jpg/icc-profiles/issue_2723.jpg"; + public const string SRgbGray = "Jpg/icc-profiles/sRGB_Gray.jpg"; + public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg"; + public const string PerceptualcLUTOnly = "Jpg/icc-profiles/Perceptual-cLUT-only.jpg"; } public static class Progressive diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png new file mode 100644 index 0000000000..a0b73d299f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe06798b92c9b476c167407e752b4379d50f1b1ad6329eceb368c8c36097b401 +size 95103 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png new file mode 100644 index 0000000000..99ae53f93e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21f8d54d4b789b783f3020402d4c1b91bb541de6565e2960976b569f60694631 +size 99385 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png new file mode 100644 index 0000000000..759b26a60c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ad361f79b4ab26d452d5cc7ada4c121dfbf45d20da7c23a58f71a9497d17a2 +size 5341 diff --git a/tests/Images/Input/Jpg/icc-profiles/Perceptual-cLUT-only.jpg b/tests/Images/Input/Jpg/icc-profiles/Perceptual-cLUT-only.jpg new file mode 100644 index 0000000000..7b2e57f659 --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/Perceptual-cLUT-only.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04e552f0bd68bddb40f35c456034b1bf1e590f37e990a28b2fe2e94753bbe685 +size 276191 diff --git a/tests/Images/Input/Jpg/icc-profiles/Perceptual.jpg b/tests/Images/Input/Jpg/icc-profiles/Perceptual.jpg new file mode 100644 index 0000000000..879fd05ad3 --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/Perceptual.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74a0931e320ca938d7dc94c4ab7b27a15880732fc139718629a7234f34bdafba +size 297456 diff --git a/tests/Images/Input/Jpg/icc-profiles/sRGB_Gray.jpg b/tests/Images/Input/Jpg/icc-profiles/sRGB_Gray.jpg new file mode 100644 index 0000000000..2abd976863 --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/sRGB_Gray.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22892d1b7965d973c7d8925ad7d749988c6a36b333b264a55d389f1e4faa0245 +size 36854