Skip to content

Commit 4d732f1

Browse files
Merge pull request #3054 from SixLabors/js/tiff-icc
Allow ICC conversion for Uncompressed CIE Lab, CMYK and RGB TIFF
2 parents 9a0b946 + 5a636fc commit 4d732f1

23 files changed

Lines changed: 529 additions & 46 deletions

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
branches:
1212
- main
1313
- release/*
14-
types: [ labeled, opened, synchronize, reopened ]
14+
types: [ opened, synchronize, reopened ]
1515

1616
jobs:
1717
# Prime a single LFS cache and expose the exact key for the matrix

src/ImageSharp/Formats/DecoderOptions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,12 @@ internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWh
7878
return false;
7979
}
8080

81-
if (profile.IsCanonicalSrgbMatrixTrc())
81+
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
8282
{
8383
return false;
8484
}
8585

86-
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
86+
if (profile.IsCanonicalSrgbMatrixTrc())
8787
{
8888
return false;
8989
}
@@ -99,11 +99,11 @@ internal bool CanRemoveIccProfile(IccProfile? profile)
9999
return false;
100100
}
101101

102-
if (this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc())
102+
if (this.ColorProfileHandling == ColorProfileHandling.Convert)
103103
{
104104
return true;
105105
}
106106

107-
return this.ColorProfileHandling == ColorProfileHandling.Convert;
107+
return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc();
108108
}
109109
}

src/ImageSharp/Formats/ImageDecoder.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ private static void HandleIccProfile(DecoderOptions options, Image image)
328328
{
329329
image.Metadata.IccProfile = null;
330330
}
331+
332+
foreach (ImageFrame frame in image.Frames)
333+
{
334+
if (options.CanRemoveIccProfile(frame.Metadata.IccProfile))
335+
{
336+
frame.Metadata.IccProfile = null;
337+
}
338+
}
331339
}
332340

333341
private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
@@ -336,5 +344,13 @@ private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
336344
{
337345
image.Metadata.IccProfile = null;
338346
}
347+
348+
foreach (ImageFrameMetadata frame in image.FrameMetadataCollection)
349+
{
350+
if (options.CanRemoveIccProfile(frame.IccProfile))
351+
{
352+
frame.IccProfile = null;
353+
}
354+
}
339355
}
340356
}

src/ImageSharp/Formats/ImageDecoderCore.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using SixLabors.ImageSharp.ColorProfiles.Icc;
66
using SixLabors.ImageSharp.IO;
77
using SixLabors.ImageSharp.Memory;
8+
using SixLabors.ImageSharp.Metadata;
89
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
910
using SixLabors.ImageSharp.PixelFormats;
1011

@@ -164,4 +165,56 @@ protected bool TryConvertIccProfile<TPixel>(Image<TPixel> image)
164165
converter.Convert(image);
165166
return true;
166167
}
168+
169+
/// <summary>
170+
/// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is
171+
/// available.
172+
/// </summary>
173+
/// <remarks>
174+
/// This method should only be used by decoders that gurantee that the encoded image data is in a color space
175+
/// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
176+
/// <br/>
177+
/// If the image does not have a valid ICC profile for color conversion, no changes are made.
178+
/// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
179+
/// space.
180+
/// </remarks>
181+
/// <typeparam name="TPixel">The pixel format.</typeparam>
182+
/// <param name="frame">The image frame whose ICC profile will be converted to the compact sRGB v4 profile.</param>
183+
/// <returns>
184+
/// <see langword="true"/> if the conversion was performed; otherwise, <see langword="false"/>.
185+
/// </returns>
186+
protected bool TryConvertIccProfile<TPixel>(ImageFrame<TPixel> frame)
187+
where TPixel : unmanaged, IPixel<TPixel>
188+
{
189+
if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile))
190+
{
191+
return false;
192+
}
193+
194+
ColorConversionOptions options = new()
195+
{
196+
SourceIccProfile = profile,
197+
TargetIccProfile = CompactSrgbV4Profile.Profile,
198+
MemoryAllocator = frame.Configuration.MemoryAllocator,
199+
};
200+
201+
ColorProfileConverter converter = new(options);
202+
203+
ImageMetadata metadata = new()
204+
{
205+
IccProfile = frame.Metadata.IccProfile
206+
};
207+
208+
IMemoryGroup<TPixel> m = frame.PixelBuffer.MemoryGroup;
209+
210+
// Safe: ToArray only materializes the Memory<TPixel> segment list, not the underlying pixel buffers,
211+
// and Wrap(Memory<T>[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just
212+
// invalidates the view). This means no pixel data is cloned and disposing the temporary image will
213+
// not dispose or leak the frame's pixel buffer.
214+
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(m.ToArray());
215+
216+
using Image<TPixel> image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata);
217+
converter.Convert(image);
218+
return true;
219+
}
167220
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers;
5+
using System.Numerics;
6+
using System.Runtime.InteropServices;
7+
using SixLabors.ImageSharp.ColorProfiles;
8+
using SixLabors.ImageSharp.ColorProfiles.Icc;
9+
using SixLabors.ImageSharp.Formats.Tiff.Utils;
10+
using SixLabors.ImageSharp.Memory;
11+
using SixLabors.ImageSharp.Metadata;
12+
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
13+
using SixLabors.ImageSharp.PixelFormats;
14+
15+
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
16+
17+
/// <summary>
18+
/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
19+
/// Each channel is represented with 16 bits.
20+
/// </summary>
21+
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
22+
internal class CieLab16PlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
23+
where TPixel : unmanaged, IPixel<TPixel>
24+
{
25+
private readonly ColorProfileConverter colorProfileConverter;
26+
private readonly Configuration configuration;
27+
private readonly bool isBigEndian;
28+
29+
// libtiff encodes 16-bit Lab as:
30+
// L* : unsigned [0, 65535] mapping to [0, 100]
31+
// a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values.
32+
private const float Inv65535 = 1f / 65535f;
33+
private const float Inv256 = 1f / 256f;
34+
35+
public CieLab16PlanarTiffColor(
36+
Configuration configuration,
37+
DecoderOptions decoderOptions,
38+
ImageFrameMetadata metadata,
39+
MemoryAllocator allocator,
40+
bool isBigEndian)
41+
{
42+
this.isBigEndian = isBigEndian;
43+
this.configuration = configuration;
44+
45+
if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile))
46+
{
47+
ColorConversionOptions options = new()
48+
{
49+
SourceIccProfile = iccProfile,
50+
TargetIccProfile = CompactSrgbV4Profile.Profile,
51+
MemoryAllocator = allocator
52+
};
53+
54+
this.colorProfileConverter = new ColorProfileConverter(options);
55+
}
56+
else
57+
{
58+
ColorConversionOptions options = new()
59+
{
60+
MemoryAllocator = allocator
61+
};
62+
63+
this.colorProfileConverter = new ColorProfileConverter(options);
64+
}
65+
}
66+
67+
/// <inheritdoc/>
68+
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
69+
{
70+
Span<byte> lPlane = data[0].GetSpan();
71+
Span<byte> aPlane = data[1].GetSpan();
72+
Span<byte> bPlane = data[2].GetSpan();
73+
74+
// Allocate temporary buffers to hold the LAB -> RGB conversion.
75+
// This should be the maximum width of a row.
76+
using IMemoryOwner<Rgb> rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Rgb>(width);
77+
using IMemoryOwner<Vector4> vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Vector4>(width);
78+
79+
Span<Rgb> rgbRow = rgbBuffer.Memory.Span;
80+
Span<Vector4> vectorRow = vectorBuffer.Memory.Span;
81+
82+
// Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
83+
Span<CieLab> cieLabRow = MemoryMarshal.Cast<Rgb, CieLab>(rgbRow);
84+
85+
int stride = width * 2;
86+
87+
if (this.isBigEndian)
88+
{
89+
for (int y = 0; y < height; y++)
90+
{
91+
int rowBase = y * stride;
92+
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);
93+
94+
for (int x = 0; x < width; x++)
95+
{
96+
int i = rowBase + (x * 2);
97+
98+
ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(lPlane.Slice(i, 2));
99+
short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(aPlane.Slice(i, 2)));
100+
short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(bPlane.Slice(i, 2)));
101+
102+
float l = lRaw * 100f * Inv65535;
103+
float a = aRaw * Inv256;
104+
float b = bRaw * Inv256;
105+
106+
cieLabRow[x] = new CieLab(l, a, b);
107+
}
108+
109+
// Convert CIE Lab -> Rgb -> Vector4 -> TPixel
110+
this.colorProfileConverter.Convert<CieLab, Rgb>(cieLabRow, rgbRow);
111+
Rgb.ToScaledVector4(rgbRow, vectorRow);
112+
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
113+
}
114+
115+
return;
116+
}
117+
118+
for (int y = 0; y < height; y++)
119+
{
120+
int rowBase = y * stride;
121+
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);
122+
123+
for (int x = 0; x < width; x++)
124+
{
125+
int i = rowBase + (x * 2);
126+
127+
ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(lPlane.Slice(i, 2));
128+
short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(aPlane.Slice(i, 2)));
129+
short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(bPlane.Slice(i, 2)));
130+
131+
float l = lRaw * 100f * Inv65535;
132+
float a = aRaw * Inv256;
133+
float b = bRaw * Inv256;
134+
135+
cieLabRow[x] = new CieLab(l, a, b);
136+
}
137+
138+
// Convert CIE Lab -> Rgb -> Vector4 -> TPixel
139+
this.colorProfileConverter.Convert<CieLab, Rgb>(cieLabRow, rgbRow);
140+
Rgb.ToScaledVector4(rgbRow, vectorRow);
141+
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)