66using System . Numerics ;
77using System . Runtime . CompilerServices ;
88using SixLabors . ImageSharp . ColorProfiles . Icc ;
9+ using SixLabors . ImageSharp . ColorSpaces . Conversion . Icc ;
910using SixLabors . ImageSharp . Memory ;
1011using SixLabors . ImageSharp . Metadata . Profiles . Icc ;
1112
@@ -28,44 +29,34 @@ internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverte
2829 throw new InvalidOperationException ( "Target ICC profile is missing." ) ;
2930 }
3031
32+ ConversionParams sourceParams = new ( converter . Options . SourceIccProfile , toPcs : true ) ;
33+ ConversionParams targetParams = new ( converter . Options . TargetIccProfile , toPcs : false ) ;
34+
3135 ColorProfileConverter pcsConverter = new ( new ColorConversionOptions
3236 {
3337 MemoryAllocator = converter . Options . MemoryAllocator ,
3438 SourceWhitePoint = new CieXyz ( converter . Options . SourceIccProfile . Header . PcsIlluminant ) ,
3539 TargetWhitePoint = new CieXyz ( converter . Options . TargetIccProfile . Header . PcsIlluminant ) ,
3640 } ) ;
3741
38- IccDataToPcsConverter sourceConverter = new ( converter . Options . SourceIccProfile ) ;
39- IccPcsToDataConverter targetConverter = new ( converter . Options . TargetIccProfile ) ;
40- IccProfileHeader sourceHeader = converter . Options . SourceIccProfile . Header ;
41- IccProfileHeader targetHeader = converter . Options . TargetIccProfile . Header ;
42- IccRenderingIntent sourceIntent = sourceHeader . RenderingIntent ;
43- IccRenderingIntent targetIntent = targetHeader . RenderingIntent ;
44- IccVersion sourceVersion = sourceHeader . Version ;
45- IccVersion targetVersion = targetHeader . Version ;
46-
47- Vector4 sourcePcs = sourceConverter . Calculate ( source . ToScaledVector4 ( ) ) ;
42+ Vector4 sourcePcs = sourceParams . Converter . Calculate ( source . ToScaledVector4 ( ) ) ;
4843 Vector4 targetPcs ;
4944
5045 // if both profiles need PCS adjustment, they both share the same unadjusted PCS space
51- // effectively cancelling out the need to make the adjustment
46+ // cancelling out the need to make the adjustment
5247 // TODO: handle PCS adjustment for absolute intent? would make this a lot more complicated
5348 // TODO: alternatively throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative intent, but so far no saturation or absolute)
54- bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent . Perceptual && sourceVersion . Major == 2 ;
55- bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent . Perceptual && targetVersion . Major == 2 ;
56- if ( adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual )
49+ if ( sourceParams . AdjustPcsForPerceptual ^ targetParams . AdjustPcsForPerceptual )
5750 {
58- targetPcs = GetTargetPcsWithPerceptualV2Adjustment ( converter , sourcePcs , adjustSourcePcsForPerceptual , adjustTargetPcsForPerceptual , pcsConverter ) ;
51+ targetPcs = GetTargetPcsWithPerceptualV2Adjustment ( sourcePcs , sourceParams , targetParams , pcsConverter ) ;
5952 }
6053 else
6154 {
62- // TODO: replace with function that bypasses PCS adjustment
63- targetPcs = GetTargetPcsWithPerceptualV2Adjustment ( converter , sourcePcs , adjustSourcePcsForPerceptual , adjustTargetPcsForPerceptual , pcsConverter ) ;
55+ targetPcs = GetTargetPcsWithoutAdjustment ( sourcePcs , sourceParams , targetParams , pcsConverter ) ;
6456 }
6557
6658 // Convert to the target space.
67- Vector4 targetValue = targetConverter . Calculate ( targetPcs ) ;
68- return TTo . FromScaledVector4 ( targetValue ) ;
59+ return TTo . FromScaledVector4 ( targetParams . Converter . Calculate ( targetPcs ) ) ;
6960 }
7061
7162 // TODO: update to match workflow of the function above
@@ -181,68 +172,129 @@ internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConvert
181172 TTo . FromScaledVector4 ( pcsNormalized , destination ) ;
182173 }
183174
184- private static Vector4 GetTargetPcsWithPerceptualV2Adjustment (
185- ColorProfileConverter converter ,
175+ private static Vector4 GetTargetPcsWithoutAdjustment (
186176 Vector4 sourcePcs ,
187- bool adjustSource ,
188- bool adjustTarget ,
177+ ConversionParams sourceParams ,
178+ ConversionParams targetParams ,
189179 ColorProfileConverter pcsConverter )
190180 {
191- IccDataToPcsConverter sourceConverter = new ( converter . Options . SourceIccProfile ! ) ;
192- IccPcsToDataConverter targetConverter = new ( converter . Options . TargetIccProfile ! ) ;
193- IccProfileHeader sourceHeader = converter . Options . SourceIccProfile ! . Header ;
194- IccProfileHeader targetHeader = converter . Options . TargetIccProfile ! . Header ;
195- IccColorSpaceType sourcePcsType = sourceHeader . ProfileConnectionSpace ;
196- IccColorSpaceType targetPcsType = targetHeader . ProfileConnectionSpace ;
181+ // Profile connecting spaces can only be Lab, XYZ.
182+ // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
183+ // so ensure that Lab is using the correct encoding when a 16-bit LUT is used
184+ switch ( sourceParams . PcsType )
185+ {
186+ // Convert from Lab to XYZ.
187+ case IccColorSpaceType . CieLab when targetParams . PcsType is IccColorSpaceType . CieXyz :
188+ {
189+ sourcePcs = sourceParams . Is16BitLutEntry ? LabV2ToLab ( sourcePcs ) : sourcePcs ;
190+ CieLab lab = CieLab . FromScaledVector4 ( sourcePcs ) ;
191+ CieXyz xyz = pcsConverter . Convert < CieLab , CieXyz > ( in lab ) ;
192+ return xyz . ToScaledVector4 ( ) ;
193+ }
194+
195+ // Convert from XYZ to Lab.
196+ case IccColorSpaceType . CieXyz when targetParams . PcsType is IccColorSpaceType . CieLab :
197+ {
198+ CieXyz xyz = CieXyz . FromScaledVector4 ( sourcePcs ) ;
199+ CieLab lab = pcsConverter . Convert < CieXyz , CieLab > ( in xyz ) ;
200+ Vector4 targetPcs = lab . ToScaledVector4 ( ) ;
201+ return targetParams . Is16BitLutEntry ? LabToLabV2 ( targetPcs ) : targetPcs ;
202+ }
203+
204+ // Convert from XYZ to XYZ.
205+ case IccColorSpaceType . CieXyz when targetParams . PcsType is IccColorSpaceType . CieXyz :
206+ {
207+ CieXyz xyz = CieXyz . FromScaledVector4 ( sourcePcs ) ;
208+ CieXyz targetXyz = pcsConverter . Convert < CieXyz , CieXyz > ( in xyz ) ;
209+ return targetXyz . ToScaledVector4 ( ) ;
210+ }
211+
212+ // Convert from Lab to Lab.
213+ case IccColorSpaceType . CieLab when targetParams . PcsType is IccColorSpaceType . CieLab :
214+ {
215+ // if both source and target LUT use same v2 LAB encoding, no need to correct them
216+ if ( sourceParams . Is16BitLutEntry && targetParams . Is16BitLutEntry )
217+ {
218+ CieLab sourceLab = CieLab . FromScaledVector4 ( sourcePcs ) ;
219+ CieLab targetLab = pcsConverter . Convert < CieLab , CieLab > ( in sourceLab ) ;
220+ return targetLab . ToScaledVector4 ( ) ;
221+ }
222+ else
223+ {
224+ sourcePcs = sourceParams . Is16BitLutEntry ? LabV2ToLab ( sourcePcs ) : sourcePcs ;
225+ CieLab sourceLab = CieLab . FromScaledVector4 ( sourcePcs ) ;
226+ CieLab targetLab = pcsConverter . Convert < CieLab , CieLab > ( in sourceLab ) ;
227+ Vector4 targetPcs = targetLab . ToScaledVector4 ( ) ;
228+ return targetParams . Is16BitLutEntry ? LabToLabV2 ( targetPcs ) : targetPcs ;
229+ }
230+ }
231+
232+ default :
233+ throw new ArgumentOutOfRangeException ( $ "Source PCS { sourceParams . PcsType } to target PCS { targetParams . PcsType } is not supported") ;
234+ }
235+ }
197236
237+ /// <summary>
238+ /// Effectively this is <see cref="GetTargetPcsWithoutAdjustment"/> with an extra step in the middle.
239+ /// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles.
240+ /// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions.
241+ /// Not required if both spaces need adjustment, since they both have the same understanding of the PCS.
242+ /// Not compatible with PCS adjustment for absolute intent.
243+ /// </summary>
244+ private static Vector4 GetTargetPcsWithPerceptualV2Adjustment (
245+ Vector4 sourcePcs ,
246+ ConversionParams sourceParams ,
247+ ConversionParams targetParams ,
248+ ColorProfileConverter pcsConverter )
249+ {
198250 // all conversions are funneled through XYZ in case PCS adjustments need to be made
199251 CieXyz xyz ;
200252
201- switch ( sourcePcsType )
253+ switch ( sourceParams . PcsType )
202254 {
203255 // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
204256 // so convert Lab to modern v4 encoding when returned from a 16-bit LUT
205257 case IccColorSpaceType . CieLab :
206- sourcePcs = sourceConverter . Is16BitLutEntry ? LabV2ToLab ( sourcePcs ) : sourcePcs ;
258+ sourcePcs = sourceParams . Is16BitLutEntry ? LabV2ToLab ( sourcePcs ) : sourcePcs ;
207259 CieLab lab = CieLab . FromScaledVector4 ( sourcePcs ) ;
208260 xyz = pcsConverter . Convert < CieLab , CieXyz > ( in lab ) ;
209261 break ;
210262 case IccColorSpaceType . CieXyz :
211263 xyz = CieXyz . FromScaledVector4 ( sourcePcs ) ;
212264 break ;
213265 default :
214- throw new ArgumentOutOfRangeException ( $ "Source PCS { sourcePcsType } not supported") ;
266+ throw new ArgumentOutOfRangeException ( $ "Source PCS { sourceParams . PcsType } is not supported") ;
215267 }
216268
217269 // when converting from device to PCS with v2 perceptual intent
218270 // the black point needs to be adjusted to v4 after converting the PCS values
219- if ( adjustSource )
271+ if ( sourceParams . AdjustPcsForPerceptual )
220272 {
221273 xyz = new CieXyz ( AdjustPcsFromV2BlackPoint ( xyz . ToVector3 ( ) ) ) ;
222274 }
223275
224276 // when converting from PCS to device with v2 perceptual intent
225277 // the black point needs to be adjusted to v2 before converting the PCS values
226- if ( adjustTarget )
278+ if ( targetParams . AdjustPcsForPerceptual )
227279 {
228280 xyz = new CieXyz ( AdjustPcsToV2BlackPoint ( xyz . ToVector3 ( ) ) ) ;
229281 }
230282
231283 Vector4 targetPcs ;
232- switch ( targetPcsType )
284+ switch ( targetParams . PcsType )
233285 {
234286 // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
235287 // so convert Lab back to legacy encoding before using in a 16-bit LUT
236288 case IccColorSpaceType . CieLab :
237289 CieLab lab = pcsConverter . Convert < CieXyz , CieLab > ( in xyz ) ;
238290 targetPcs = lab . ToScaledVector4 ( ) ;
239- targetPcs = targetConverter . Is16BitLutEntry ? LabToLabV2 ( targetPcs ) : targetPcs ;
291+ targetPcs = targetParams . Is16BitLutEntry ? LabToLabV2 ( targetPcs ) : targetPcs ;
240292 break ;
241293 case IccColorSpaceType . CieXyz :
242294 targetPcs = xyz . ToScaledVector4 ( ) ;
243295 break ;
244296 default :
245- throw new ArgumentOutOfRangeException ( $ "Target PCS { targetPcsType } not supported") ;
297+ throw new ArgumentOutOfRangeException ( $ "Target PCS { targetParams . PcsType } is not supported") ;
246298 }
247299
248300 return targetPcs ;
@@ -314,4 +366,29 @@ private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [C
314366 }
315367 }
316368 }
369+
370+ private class ConversionParams
371+ {
372+ private readonly IccProfile profile ;
373+
374+ internal ConversionParams ( IccProfile profile , bool toPcs )
375+ {
376+ this . profile = profile ;
377+ this . Converter = toPcs ? new IccDataToPcsConverter ( profile ) : new IccPcsToDataConverter ( profile ) ;
378+ }
379+
380+ internal IccConverterBase Converter { get ; }
381+
382+ internal IccProfileHeader Header => this . profile . Header ;
383+
384+ internal IccRenderingIntent Intent => this . Header . RenderingIntent ;
385+
386+ internal IccColorSpaceType PcsType => this . Header . ProfileConnectionSpace ;
387+
388+ internal IccVersion Version => this . Header . Version ;
389+
390+ internal bool AdjustPcsForPerceptual => this . Intent == IccRenderingIntent . Perceptual && this . Version . Major == 2 ;
391+
392+ internal bool Is16BitLutEntry => this . Converter . Is16BitLutEntry ;
393+ }
317394}
0 commit comments