From 82eb56b1180ec1e509219b2e3732841db7d3dff9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Jan 2026 15:26:26 +1000 Subject: [PATCH] Basic fallback functionality complete --- .../ColorProfileConverterExtensionsIcc.cs | 41 +++++++++++ ...ofileConverterExtensionsPixelCompatible.cs | 72 +++++++++++++++++++ src/ImageSharp/Formats/ImageDecoderCore.cs | 40 +++++++++++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 53 ++------------ .../MemoryAllocatorValidator.cs | 35 +++++---- 5 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index 3ddbf93b5..fd99fb446 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -39,6 +39,24 @@ internal static class ColorProfileConverterExtensionsIcc 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F]; + /// + /// Converts a color value from one ICC color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion using ICC profiles, ensuring accurate color mapping + /// between different color spaces. Both the source and target ICC profiles must be provided in the converter's + /// options. The method supports perceptual adjustments when required by the profiles. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter configured with source and target ICC profiles. + /// The color value to convert, defined in the source color profile. + /// + /// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value. + /// + /// + /// Thrown if either the source or target ICC profile is missing from the converter options. + /// internal static TTo ConvertUsingIccProfile(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -81,6 +99,29 @@ internal static class ColorProfileConverterExtensionsIcc return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); } + /// + /// Converts a span of color values from a source color profile to a destination color profile using ICC profiles. + /// + /// + /// This method performs color conversion by transforming the input values through the Profile + /// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required + /// by the profiles. The method does not support absolute colorimetric intent and will not perform such + /// conversions. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter that provides conversion options and ICC profiles. + /// + /// A read-only span containing the source color values to convert. The values must conform to the source color + /// profile. + /// + /// + /// A span to receive the converted color values in the destination color profile. Must be at least as large as the + /// source span. + /// + /// + /// Thrown if the source or target ICC profile is missing from the converter options. + /// internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs new file mode 100644 index 000000000..5f3d42afb --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsPixelCompatible +{ + /// + /// Converts the pixel data of the specified image from the source color profile to the target color profile using + /// the provided color profile converter. + /// + /// + /// This method modifies the source image in place by converting its pixel data according to the + /// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible; + /// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC + /// profiles are set on the converter before calling this method. + /// + /// The pixel format. + /// The color profile converter configured with source and target ICC profiles. + /// + /// The image whose pixel data will be converted. The conversion is performed in place, modifying the original + /// image. + /// + /// + /// Thrown if the converter's source or target ICC profile is not specified. + /// + public static void Convert(this ColorProfileConverter converter, Image source) + where TPixel : unmanaged, IPixel + { + // These checks actually take place within the converter, but we want to fail fast here. + // Note. we do not check to see whether the profiles themselves are RGB compatible, + // if they are not, then the converter will simply produce incorrect results. + if (converter.Options.SourceIccProfile is null) + { + throw new InvalidOperationException("Source ICC profile is missing."); + } + + if (converter.Options.TargetIccProfile is null) + { + throw new InvalidOperationException("Target ICC profile is missing."); + } + + // Process the rows in parallel chnks, the converter itself is thread safe. + source.Mutate(o => o.ProcessPixelRowsAsVector4( + row => + { + // Gather and convert the pixels in the row to Rgb. + using IMemoryOwner rgbBuffer = converter.Options.MemoryAllocator.Allocate(row.Length); + Span rgbSpan = rgbBuffer.Memory.Span; + Rgb.FromScaledVector4(row, rgbSpan); + + // Perform the actual color conversion. + converter.ConvertUsingIccProfile(rgbSpan, rgbSpan); + + // Copy the converted Rgb pixels back to the row as TPixel. + ref Vector4 rowRef = ref MemoryMarshal.GetReference(row); + for (int i = 0; i < rgbSpan.Length; i++) + { + Vector3 rgb = rgbSpan[i].AsVector3Unsafe(); + Unsafe.As(ref Unsafe.Add(ref rowRef, i)) = rgb; + } + }, + PixelConversionModifiers.Scale)); + } +} diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index adf0107da..da50a1abe 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -124,4 +127,41 @@ internal abstract class ImageDecoderCore /// protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; + + /// + /// Converts the ICC color profile of the specified image to the compact sRGB v4 profile if a source profile is + /// available. + /// + /// + /// This method should only be used by decoders that gurantee that the encoded image data is in a color space + /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB). + ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made. + /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color + /// space. + ///
+ /// The pixel format. + /// The image whose ICC profile will be converted to the compact sRGB v4 profile. + /// + /// if the conversion was performed; otherwise, . + /// + protected bool TryConvertIccProfile(Image image) + where TPixel : unmanaged, IPixel + { + if (!this.Options.TryGetIccProfileForColorConversion(image.Metadata.IccProfile, out IccProfile? profile)) + { + return false; + } + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = image.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + converter.Convert(image); + return true; + } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index b0a84341f..4c9bd6f32 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -7,12 +7,9 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Compression; using System.IO.Hashing; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -26,7 +23,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Png; @@ -234,9 +230,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) { - metadata.IccProfile = null; + // TODO: Rework this. We need to preserve metadata + // metadata.IccProfile = null; } this.currentStream.Position += 4; @@ -271,9 +268,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) { - metadata.IccProfile = null; + // TODO: Rework this. We need to preserve metadata + // metadata.IccProfile = null; } this.ReadScanlines( @@ -345,11 +343,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore PngThrowHelper.ThrowNoData(); } - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfileToApply)) - { - ApplyRgbaCompatibleIccProfile(image, iccProfileToApply, CompactSrgbV4Profile.Profile); - } - + _ = this.TryConvertIccProfile(image); return image; } catch @@ -2201,37 +2195,4 @@ internal sealed class PngDecoderCore : ImageDecoderCore private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); - - private static void ApplyRgbaCompatibleIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) - where TPixel : unmanaged, IPixel - { - ColorConversionOptions options = new() - { - SourceIccProfile = sourceProfile, - TargetIccProfile = destinationProfile, - MemoryAllocator = image.Configuration.MemoryAllocator, - }; - - ColorProfileConverter converter = new(options); - - image.Mutate(o => o.ProcessPixelRowsAsVector4( - (pixelsRow, _) => - { - using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); - Span rgbPacked = rgbBuffer.Memory.Span; - - Rgb.FromScaledVector4(pixelsRow, rgbPacked); - converter.Convert(rgbPacked, rgbPacked); - - Span pixelsRowAsFloats = MemoryMarshal.Cast(pixelsRow); - ref float pixelsRowAsFloatsRef = ref MemoryMarshal.GetReference(pixelsRowAsFloats); - - int cIdx = 0; - for (int x = 0; x < pixelsRow.Length; x++, cIdx += 4) - { - Unsafe.As(ref Unsafe.Add(ref pixelsRowAsFloatsRef, cIdx)) = rgbPacked[x]; - } - }, - PixelConversionModifiers.Scale)); - } } diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs index 2afa5fdc9..6c8dd2618 100644 --- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -20,26 +20,13 @@ public static class MemoryAllocatorValidator private static void MemoryDiagnostics_MemoryReleased() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - lock (backing) - { - backing.TotalRemainingAllocated--; - } - } + backing?.OnReleased(); } private static void MemoryDiagnostics_MemoryAllocated() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - lock (backing) - { - backing.TotalAllocated++; - backing.TotalRemainingAllocated++; - } - } + backing?.OnAllocated(); } public static TestMemoryDiagnostics MonitorAllocations() @@ -54,11 +41,23 @@ public static class MemoryAllocatorValidator public static void ValidateAllocations(int expectedAllocationCount = 0) => LocalInstance.Value?.Validate(expectedAllocationCount); - public class TestMemoryDiagnostics : IDisposable + public sealed class TestMemoryDiagnostics : IDisposable { - public int TotalAllocated { get; set; } + private int totalAllocated; + private int totalRemainingAllocated; + + public int TotalAllocated => Volatile.Read(ref this.totalAllocated); + + public int TotalRemainingAllocated => Volatile.Read(ref this.totalRemainingAllocated); + + internal void OnAllocated() + { + Interlocked.Increment(ref this.totalAllocated); + Interlocked.Increment(ref this.totalRemainingAllocated); + } - public int TotalRemainingAllocated { get; set; } + internal void OnReleased() + => Interlocked.Decrement(ref this.totalRemainingAllocated); public void Validate(int expectedAllocationCount) {