Browse Source

Add ability to convert ICC profile on decode

pull/1567/head
James Jackson-South 3 years ago
parent
commit
66554cba67
  1. 34
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs
  2. 45
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/SrgbV4Profile.Generated.cs
  3. 4
      src/ImageSharp/Formats/ColorProfileHandling.cs
  4. 5
      src/ImageSharp/Formats/DecoderOptions.cs
  5. 65
      src/ImageSharp/Formats/ImageDecoder.cs
  6. 42
      src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs
  7. 16
      tests/ImageSharp.Tests/Colorspaces/Icc/IccProfileConverterTests.cs
  8. 8
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  9. 4
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

34
src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/IccProfileConverter.cs

@ -12,10 +12,19 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.Icc;
/// <summary>
/// Allows the copnversion between ICC profiles.
/// Allows the conversion between ICC profiles.
/// </summary>
internal static class IccProfileConverter
{
/// <summary>
/// Performs a conversion of the image pixels based on the input and output ICC profiles.
/// </summary>
/// <param name="image">The image to convert.</param>
/// <param name="inputIccProfile">The input ICC profile.</param>
/// <param name="outputIccProfile">The output ICC profile. </param>
public static void Convert(Image image, IccProfile? inputIccProfile, IccProfile? outputIccProfile)
=> image.AcceptVisitor(new IccProfileConverterVisitor(inputIccProfile, outputIccProfile));
/// <summary>
/// Performs a conversion of the image pixels based on the input and output ICC profiles.
/// </summary>
@ -23,11 +32,10 @@ internal static class IccProfileConverter
/// <param name="image">The image to convert.</param>
/// <param name="inputIccProfile">The input ICC profile.</param>
/// <param name="outputIccProfile">The output ICC profile. </param>
public static void Convert<TPixel>(Image<TPixel> image, IccProfile inputIccProfile, IccProfile outputIccProfile)
where TPixel : unmanaged, IPixel<TPixel>
public static void Convert<TPixel>(Image<TPixel> image, IccProfile? inputIccProfile, IccProfile? outputIccProfile)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> => Convert(image, this.inputIccProfile, this.outputIccProfile);
}
}

45
src/ImageSharp/ColorSpaces/Conversion/Implementation/Icc/SrgbV4Profile.Generated.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// <auto-generated />
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<byte> 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<IccProfile> 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);
}
}

4
src/ImageSharp/Formats/ColorProfileHandling.cs

@ -14,8 +14,8 @@ public enum ColorProfileHandling
Preserve,
/// <summary>
/// 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.
/// </summary>
Convert
}

5
src/ImageSharp/Formats/DecoderOptions.cs

@ -44,4 +44,9 @@ public sealed class DecoderOptions
/// Gets the maximum number of image frames to decode, inclusive.
/// </summary>
public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); }
/// <summary>
/// Gets a value that controls how ICC profiles are handled during decode.
/// </summary>
public ColorProfileHandling ColorProfileHandling { get; init; }
}

65
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
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(
options,
stream,
s => this.Decode<TPixel>(options, s, default));
{
Image<TPixel> image = WithSeekableStream(
options,
stream,
s => this.Decode<TPixel>(options, s, default));
TransformColorProfile(options, image);
return image;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
public async Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableMemoryStreamAsync(
{
Image<TPixel> image = await WithSeekableMemoryStreamAsync(
options,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken);
cancellationToken).ConfigureAwait(false);
TransformColorProfile(options, image);
return image;
}
/// <inheritdoc/>
public Task<Image> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
=> WithSeekableMemoryStreamAsync(
public async Task<Image> 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;
}
/// <inheritdoc/>
public IImageInfo Identify(DecoderOptions options, Stream stream)
@ -123,6 +145,21 @@ public abstract class ImageDecoder : IImageDecoder
}
}
/// <summary>
/// Converts the decoded image color profile if present to a V4 sRGB profile.
/// </summary>
/// <param name="options">The decoder options.</param>
/// <param name="image">The image.</param>
protected static void TransformColorProfile(DecoderOptions options, Image image)
{
if (options.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return;
}
IccProfileConverter.Convert(image, image.Metadata?.IccProfile, SrgbV4Profile.GetProfile());
}
/// <summary>
/// Determines whether the decoded image should be resized.
/// </summary>

42
src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs

@ -17,34 +17,54 @@ public abstract class SpecializedImageDecoder<T> : ImageDecoder, ISpecializedIma
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(T options, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(
options.GeneralOptions,
stream,
s => this.Decode<TPixel>(options, s, default));
{
Image<TPixel> image = WithSeekableStream(
options.GeneralOptions,
stream,
s => this.Decode<TPixel>(options, s, default));
TransformColorProfile(options.GeneralOptions, image);
return image;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public Task<Image<TPixel>> DecodeAsync<TPixel>(T options, Stream stream, CancellationToken cancellationToken = default)
public async Task<Image<TPixel>> DecodeAsync<TPixel>(T options, Stream stream, CancellationToken cancellationToken = default)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableMemoryStreamAsync(
{
Image<TPixel> image = await WithSeekableMemoryStreamAsync(
options.GeneralOptions,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken);
cancellationToken).ConfigureAwait(false);
TransformColorProfile(options.GeneralOptions, image);
return image;
}
/// <inheritdoc/>
public Task<Image> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken = default)
=> WithSeekableMemoryStreamAsync(
public async Task<Image> 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;
}
/// <summary>
/// Decodes the image from the specified stream to an <see cref="Image{TPixel}" /> of a specific pixel type.

16
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<TPixel>(TestImageProvider<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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);
}
}

8
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;

4
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;
}

Loading…
Cancel
Save