diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs
index 6325c5ce1..8676f4afb 100644
--- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs
+++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs
@@ -12,10 +12,19 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.Icc;
///
-/// Allows the copnversion between ICC profiles.
+/// Allows the conversion between ICC profiles.
///
internal static class IccProfileConverter
{
+ ///
+ /// Performs a conversion of the image pixels based on the input and output ICC profiles.
+ ///
+ /// The image to convert.
+ /// The input ICC profile.
+ /// The output ICC profile.
+ public static void Convert(Image image, IccProfile? inputIccProfile, IccProfile? outputIccProfile)
+ => image.AcceptVisitor(new IccProfileConverterVisitor(inputIccProfile, outputIccProfile));
+
///
/// Performs a conversion of the image pixels based on the input and output ICC profiles.
///
@@ -23,11 +32,10 @@ internal static class IccProfileConverter
/// The image to convert.
/// The input ICC profile.
/// The output ICC profile.
- public static void Convert(Image image, IccProfile inputIccProfile, IccProfile outputIccProfile)
- where TPixel : unmanaged, IPixel
+ public static void Convert(Image image, IccProfile? inputIccProfile, IccProfile? outputIccProfile)
+ where TPixel : unmanaged, IPixel
{
- // TODO: Is this the correct property?
- if (inputIccProfile.Header.Id.Equals(outputIccProfile.Header.Id))
+ if (inputIccProfile is null || outputIccProfile is null)
{
return;
}
@@ -55,7 +63,21 @@ internal static class IccProfileConverter
}
});
- // TODO: Do not preserve the profile if we are converting to sRGB.
image.Metadata.IccProfile = outputIccProfile;
}
+
+ private readonly struct IccProfileConverterVisitor : IImageVisitor
+ {
+ private readonly IccProfile? inputIccProfile;
+ private readonly IccProfile? outputIccProfile;
+
+ public IccProfileConverterVisitor(IccProfile? inputIccProfile, IccProfile? outputIccProfile)
+ {
+ this.inputIccProfile = inputIccProfile;
+ this.outputIccProfile = outputIccProfile;
+ }
+
+ public void Visit(Image image)
+ where TPixel : unmanaged, IPixel => Convert(image, this.inputIccProfile, this.outputIccProfile);
+ }
}
diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/SrgbV4Profile.Generated.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/SrgbV4Profile.Generated.cs
new file mode 100644
index 000000000..45c231aa6
--- /dev/null
+++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/SrgbV4Profile.Generated.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+//
+
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+
+namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;
+
+internal static class SrgbV4Profile
+{
+ // Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles
+ private static ReadOnlySpan Data => new byte[]
+ {
+ 0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0,
+ 20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171,
+ 223, 92, 167, 3, 18, 168, 85, 164, 236, 53, 122, 209, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 100, 101, 115, 99, 0, 0, 0, 252, 0, 0, 0, 36, 99,
+ 112, 114, 116, 0, 0, 1, 32, 0, 0, 0, 34, 119, 116, 112, 116, 0, 0, 1, 68, 0, 0, 0, 20, 99, 104, 97, 100, 0, 0,
+ 1, 88, 0, 0, 0, 44, 114, 88, 89, 90, 0, 0, 1, 132, 0, 0, 0, 20, 103, 88, 89, 90, 0, 0, 1, 152, 0, 0, 0,
+ 20, 98, 88, 89, 90, 0, 0, 1, 172, 0, 0, 0, 20, 114, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 103, 84, 82, 67,
+ 0, 0, 1, 192, 0, 0, 0, 32, 98, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 109, 108, 117, 99, 0, 0, 0, 0, 0,
+ 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 8, 0, 0, 0, 28, 0, 115, 0, 82, 0, 71, 0, 66, 109, 108,
+ 117, 99, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 6, 0, 0, 0, 28, 0, 67, 0,
+ 67, 0, 48, 0, 33, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 115, 102, 51, 50,
+ 0, 0, 0, 0, 0, 1, 12, 63, 0, 0, 5, 221, 255, 255, 243, 38, 0, 0, 7, 144, 0, 0, 253, 146, 255, 255, 251, 161, 255,
+ 255, 253, 162, 0, 0, 3, 220, 0, 0, 192, 113, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 111, 160, 0, 0, 56, 242, 0, 0,
+ 3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0,
+ 0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105,
+ 0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91,
+ };
+
+ private static readonly Lazy LazyIccProfile = new(() => GetIccProfile());
+
+ public static IccProfile GetProfile() => LazyIccProfile.Value;
+
+ private static IccProfile GetIccProfile()
+ {
+ byte[] buffer = new byte[Data.Length];
+ Data.CopyTo(buffer);
+ return new IccProfile(buffer);
+ }
+}
+
diff --git a/src/ImageSharp/Formats/ColorProfileHandling.cs b/src/ImageSharp/Formats/ColorProfileHandling.cs
index de1a765a9..e6f4b0a6a 100644
--- a/src/ImageSharp/Formats/ColorProfileHandling.cs
+++ b/src/ImageSharp/Formats/ColorProfileHandling.cs
@@ -14,8 +14,8 @@ public enum ColorProfileHandling
Preserve,
///
- /// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB.
- /// The original profile is then removed.
+ /// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB V4 profile.
+ /// The original profile is then replaced.
///
Convert
}
diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs
index 989fc49fc..80c4f2933 100644
--- a/src/ImageSharp/Formats/DecoderOptions.cs
+++ b/src/ImageSharp/Formats/DecoderOptions.cs
@@ -44,4 +44,9 @@ public sealed class DecoderOptions
/// Gets the maximum number of image frames to decode, inclusive.
///
public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); }
+
+ ///
+ /// Gets a value that controls how ICC profiles are handled during decode.
+ ///
+ public ColorProfileHandling ColorProfileHandling { get; init; }
}
diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs
index 591f85df2..30a87acd6 100644
--- a/src/ImageSharp/Formats/ImageDecoder.cs
+++ b/src/ImageSharp/Formats/ImageDecoder.cs
@@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
#nullable disable
+using SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;
+using SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.Icc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
@@ -17,34 +19,54 @@ public abstract class ImageDecoder : IImageDecoder
///
public Image Decode(DecoderOptions options, Stream stream)
where TPixel : unmanaged, IPixel
- => WithSeekableStream(
- options,
- stream,
- s => this.Decode(options, s, default));
+ {
+ Image image = WithSeekableStream(
+ options,
+ stream,
+ s => this.Decode(options, s, default));
+
+ TransformColorProfile(options, image);
+ return image;
+ }
///
public Image Decode(DecoderOptions options, Stream stream)
- => WithSeekableStream(
- options,
- stream,
- s => this.Decode(options, s, default));
+ {
+ Image image = WithSeekableStream(
+ options,
+ stream,
+ s => this.Decode(options, s, default));
+
+ TransformColorProfile(options, image);
+ return image;
+ }
///
- public Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
+ public async Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
where TPixel : unmanaged, IPixel
- => WithSeekableMemoryStreamAsync(
+ {
+ Image image = await WithSeekableMemoryStreamAsync(
options,
stream,
(s, ct) => this.Decode(options, s, ct),
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+
+ TransformColorProfile(options, image);
+ return image;
+ }
///
- public Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
- => WithSeekableMemoryStreamAsync(
+ public async Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
+ {
+ Image image = await WithSeekableMemoryStreamAsync(
options,
stream,
(s, ct) => this.Decode(options, s, ct),
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+
+ TransformColorProfile(options, image);
+ return image;
+ }
///
public IImageInfo Identify(DecoderOptions options, Stream stream)
@@ -123,6 +145,21 @@ public abstract class ImageDecoder : IImageDecoder
}
}
+ ///
+ /// Converts the decoded image color profile if present to a V4 sRGB profile.
+ ///
+ /// The decoder options.
+ /// The image.
+ protected static void TransformColorProfile(DecoderOptions options, Image image)
+ {
+ if (options.ColorProfileHandling == ColorProfileHandling.Preserve)
+ {
+ return;
+ }
+
+ IccProfileConverter.Convert(image, image.Metadata?.IccProfile, SrgbV4Profile.GetProfile());
+ }
+
///
/// Determines whether the decoded image should be resized.
///
diff --git a/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs b/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs
index fa6461464..ee602e7e2 100644
--- a/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs
+++ b/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs
@@ -17,34 +17,54 @@ public abstract class SpecializedImageDecoder : ImageDecoder, ISpecializedIma
///
public Image Decode(T options, Stream stream)
where TPixel : unmanaged, IPixel
- => WithSeekableStream(
- options.GeneralOptions,
- stream,
- s => this.Decode(options, s, default));
+ {
+ Image image = WithSeekableStream(
+ options.GeneralOptions,
+ stream,
+ s => this.Decode(options, s, default));
+
+ TransformColorProfile(options.GeneralOptions, image);
+ return image;
+ }
///
public Image Decode(T options, Stream stream)
- => WithSeekableStream(
+ {
+ Image image = WithSeekableStream(
options.GeneralOptions,
stream,
s => this.Decode(options, s, default));
+ TransformColorProfile(options.GeneralOptions, image);
+ return image;
+ }
+
///
- public Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default)
+ public async Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default)
where TPixel : unmanaged, IPixel
- => WithSeekableMemoryStreamAsync(
+ {
+ Image image = await WithSeekableMemoryStreamAsync(
options.GeneralOptions,
stream,
(s, ct) => this.Decode(options, s, ct),
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+
+ TransformColorProfile(options.GeneralOptions, image);
+ return image;
+ }
///
- public Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default)
- => WithSeekableMemoryStreamAsync(
+ public async Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default)
+ {
+ Image image = await WithSeekableMemoryStreamAsync(
options.GeneralOptions,
stream,
(s, ct) => this.Decode(options, s, ct),
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+
+ TransformColorProfile(options.GeneralOptions, image);
+ return image;
+ }
///
/// Decodes the image from the specified stream to an of a specific pixel type.
diff --git a/tests/ImageSharp.Tests/Colorspaces/Icc/IccProfileConverterTests.cs b/tests/ImageSharp.Tests/Colorspaces/Icc/IccProfileConverterTests.cs
index 67aa347b2..964b2b41e 100644
--- a/tests/ImageSharp.Tests/Colorspaces/Icc/IccProfileConverterTests.cs
+++ b/tests/ImageSharp.Tests/Colorspaces/Icc/IccProfileConverterTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;
using SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.Icc;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@@ -29,7 +30,7 @@ public class IccProfileConverterTests
IccProfileConverter.Convert(image, profile, profile);
- image.DebugSave(provider, Encoder);
+ image.DebugSave(provider, extension: "png", appendPixelTypeToFileName: false, appendSourceFileOrDescription: true, encoder: Encoder);
TPixel actual = image[0, 0];
@@ -38,19 +39,22 @@ public class IccProfileConverterTests
[Theory]
[WithFile(TestImages.Jpeg.ICC.AdobeRgb, PixelTypes.Rgb24)]
- public void CanConvertToWide(TestImageProvider provider)
+ [WithFile(TestImages.Jpeg.ICC.AppleRGB, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.ICC.ColorMatch, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.ICC.WideRGB, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.ICC.SRgb, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.ICC.ProPhoto, PixelTypes.Rgb24)]
+ public void CanConvertToSRGB(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
using Image image = provider.GetImage();
IccProfile profile = image.Metadata.IccProfile;
- string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.ICC.SRgb);
- IImageInfo i = Image.Identify(file);
- IccProfile sRGBProfile = i.Metadata.IccProfile;
+ IccProfile sRGBProfile = SrgbV4Profile.GetProfile();
IccProfileConverter.Convert(image, profile, sRGBProfile);
// TODO: Compare.
- image.DebugSave(provider, Encoder);
+ image.DebugSave(provider, extension: "png", appendPixelTypeToFileName: false, appendSourceFileOrDescription: true, encoder: Encoder);
}
}
diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
index 460ecac85..b3927de41 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
@@ -49,8 +49,8 @@ public class ImagingTestCaseUtility
}
string fn = appendSourceFileOrDescription
- ? Path.GetFileNameWithoutExtension(this.SourceFileOrDescription)
- : string.Empty;
+ ? Path.GetFileNameWithoutExtension(this.SourceFileOrDescription)
+ : string.Empty;
if (string.IsNullOrWhiteSpace(extension))
{
@@ -62,7 +62,7 @@ public class ImagingTestCaseUtility
extension = ".bmp";
}
- extension = extension.ToLower();
+ extension = extension.ToLowerInvariant();
if (extension[0] != '.')
{
@@ -86,7 +86,7 @@ public class ImagingTestCaseUtility
}
}
- details = details ?? string.Empty;
+ details ??= string.Empty;
if (details != string.Empty)
{
details = '_' + details;
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
index 31c9f541e..30e76831d 100644
--- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
@@ -67,10 +67,10 @@ public static class TestImageExtensions
provider.Utility.SaveTestOutputFile(
image,
extension,
+ encoder: encoder,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName,
- appendSourceFileOrDescription: appendSourceFileOrDescription,
- encoder: encoder);
+ appendSourceFileOrDescription: appendSourceFileOrDescription);
return image;
}