From 72cfecd3cae2d4eb9c07b93bbc97245b9b9dfbc1 Mon Sep 17 00:00:00 2001 From: Max Derhak Date: Fri, 7 Aug 2026 13:27:41 -0400 Subject: [PATCH] Add opt-in iccMAX support for hybrid printer profiles with reflectance Implements the ICS for HybridPrinterWithReflectance, so a caller can read and author the spectral reflectance transforms of an ICC.2 hybrid printer profile. All of it sits behind CMS_USE_ICCMAX_SPECTRAL, which is undefined by default; a default build is behaviourally identical to master. Added, all guarded: - extendedCLUTElement ('xclt') in its four value encodings, and singleSampledCurve ('sngf') - the five ICC.2-only formulaCurveSegment function types, 0003h to 0007h, as parametric types 9 to 13 - the spectral PCS header fields, with accessors - an embedded ICC.2 profile in an 'ICC5' tag - spectralWhitePointTag ('swpt') in all three permitted encodings, including uInt16ArrayType via a pair of accessors - spectralViewingConditionsTag ('svcn') - float16ArrayType ('fl16') and float32ArrayType ('fl32') - a 66 KB hybrid printer test profile and the script that produced it Two places where ICC.2 disagrees with the reference implementation are resolved in favour of the reference, since interop follows it, and both are documented at the site: spectralViewingConditions stores its two trailing CIEXYZ triples as float32 rather than the XYZNumber Table 69 specifies, and the 1/65535 scaling for a uInt16-encoded swpt appears nowhere in ICC.2 at all. One change is unconditional and deliberate. Raising the extendedCLUT output-channel ceiling invalidated the overflow guard inside CubeSize, where the constant 15 was load-bearing because it matched the old parser limit, so a crafted profile could wrap the allocation size and be read out of bounds. The overflow-safe multiply now lives in cmsStageAllocCLutFloatGranular and cmsStageAllocCLut16bitGranular unconditionally, because that weakness is reachable in master through the public API. A default build therefore differs from master in one respect: input master would accept and then corrupt is now rejected. CMS_USE_ICCMAX_SPECTRAL and CMS_NO_HALF_SUPPORT are mutually exclusive and produce an #error, because ICC.2 encodes spectralRange as float16Number. The test profile's DToB3 carries a curve set of four shaper curves: three singleSampledCurves and, deliberately, one segmentedCurve holding an ICC.2 formulaCurveSegment of function type 0003h with identity parameters, so a single fixture exercises both curve encodings and an ICC.2-only formula type without changing any output value. It is curve 0 rather than the last so the curves after it shift in the position table. Its header declares version 5.1 rather than 5.0, which is the case that proves the version check tolerates a minor bump. Tests: 158 with the macro undefined, unchanged from master, and 174 with it defined. --- include/lcms2.h | 109 + src/cmsgamma.c | 127 +- src/cmsio0.c | 366 ++++ src/cmslut.c | 39 +- src/cmsplugin.c | 37 + src/cmstypes.c | 1082 +++++++++- src/lcms2_internal.h | 37 + testbed/HybridPrinterCMYK_small.icc | Bin 0 -> 66552 bytes testbed/Makefile.am | 3 +- testbed/Makefile.in | 3 +- testbed/meson.build | 2 +- testbed/subsample_clut.py | 236 +++ testbed/testcms2.c | 2907 +++++++++++++++++++++++++++ 13 files changed, 4919 insertions(+), 29 deletions(-) create mode 100644 testbed/HybridPrinterCMYK_small.icc create mode 100644 testbed/subsample_clut.py diff --git a/include/lcms2.h b/include/lcms2.h index a298cca62..c6ddd740c 100644 --- a/include/lcms2.h +++ b/include/lcms2.h @@ -62,6 +62,19 @@ // Uncomment to get rid of the tables for "half" float support // #define CMS_NO_HALF_SUPPORT 1 +// Uncomment to enable reading iccMAX (ICC.2) spectral data embedded in ICC.1 +// profiles. Off by default: it widens the set of profiles, tags and processing +// elements the parser accepts. +// #define CMS_USE_ICCMAX_SPECTRAL 1 + +// iccMAX support cannot be built without half float support: ICC.2 encodes the +// header's spectralRange as float16Number (ICC.2:2023 4.2.8), so with the tables +// gone a spectral PCS cannot be read or written at all, float16ArrayType is +// unavailable, and the spectral white point tag cannot be parsed. +#if defined(CMS_USE_ICCMAX_SPECTRAL) && defined(CMS_NO_HALF_SUPPORT) +# error "CMS_USE_ICCMAX_SPECTRAL requires half float support: ICC.2 encodes spectralRange as float16Number, so CMS_NO_HALF_SUPPORT and CMS_USE_ICCMAX_SPECTRAL are mutually exclusive. Undefine one of them." +#endif + // Uncomment to get rid of pthreads/windows dependency // #define CMS_NO_PTHREADS 1 @@ -345,6 +358,13 @@ typedef enum { cmsSigViewingConditionsType = 0x76696577, // 'view' cmsSigXYZType = 0x58595A20, // 'XYZ ' cmsSigMHC2Type = 0x4D484332 // 'MHC2' +#ifdef CMS_USE_ICCMAX_SPECTRAL + , + cmsSigEmbeddedProfileType = 0x49434370, // 'ICCp', holds an embedded ICC.2 profile + cmsSigFloat16ArrayType = 0x666C3136, // 'fl16', ICC.2 10.2.9 float16ArrayType + cmsSigFloat32ArrayType = 0x666C3332, // 'fl32', ICC.2 10.2.10 float32ArrayType + cmsSigSpectralViewingConditionsType = 0x7376636E // 'svcn', ICC.2 10.2.22 +#endif } cmsTagTypeSignature; @@ -424,6 +444,12 @@ typedef enum { cmsSigcicpTag = 0x63696370, // 'cicp' cmsSigArgyllArtsTag = 0x61727473, // 'arts' cmsSigMHC2Tag = 0x4D484332 // 'MHC2' +#ifdef CMS_USE_ICCMAX_SPECTRAL + , + cmsSigEmbeddedV5ProfileTag = 0x49434335, // 'ICC5', an embedded ICC.2 profile + cmsSigSpectralWhitePointTag = 0x73777074, // 'swpt', ICC.2 9.2.112 + cmsSigSpectralViewingConditionsTag = 0x7376636E // 'svcn', ICC.2 9.2.111 +#endif } cmsTagSignature; @@ -552,6 +578,9 @@ typedef enum { cmsSigCurveSetElemType = 0x63767374, //'cvst' cmsSigMatrixElemType = 0x6D617466, //'matf' cmsSigCLutElemType = 0x636C7574, //'clut' +#ifdef CMS_USE_ICCMAX_SPECTRAL + cmsSigExtCLutElemType = 0x78636C74, //'xclt' (ICC.2 extendedCLUTElement) +#endif cmsSigBAcsElemType = 0x62414353, // 'bACS' cmsSigEAcsElemType = 0x65414353, // 'eACS' @@ -581,6 +610,10 @@ typedef enum { cmsSigFormulaCurveSeg = 0x70617266, // 'parf' cmsSigSampledCurveSeg = 0x73616D66, // 'samf' cmsSigSegmentedCurve = 0x63757266 // 'curf' +#ifdef CMS_USE_ICCMAX_SPECTRAL + , + cmsSigSingleSampledCurve = 0x736E6766 // 'sngf' (ICC.2 singleSampledCurve) +#endif } cmsCurveSegSignature; @@ -1564,6 +1597,82 @@ CMSAPI void CMSEXPORT cmsSetHeaderRenderingIntent(cmsHPROFILE hProf CMSAPI cmsColorSpaceSignature CMSEXPORT cmsGetPCS(cmsHPROFILE hProfile); CMSAPI void CMSEXPORT cmsSetPCS(cmsHPROFILE hProfile, cmsColorSpaceSignature pcs); + +#ifdef CMS_USE_ICCMAX_SPECTRAL +// An array of spectral values, as carried by the ICC.2 float16ArrayType and +// float32ArrayType tag types. The count lives here rather than in a tag +// descriptor, because ICC.2 derives it from the tag size. +typedef struct { + cmsContext ContextID; + cmsUInt32Number nValues; + cmsFloat32Number* Values; + + // Both fl16 and fl32 are read; the tag is always written back as fl32, + // because fl32 is lossless from fl16 and Little-CMS does not preserve + // tag encodings. + +} cmsFloatArray; + +CMSAPI cmsFloatArray* CMSEXPORT cmsAllocFloatArray(cmsContext ContextID, cmsUInt32Number nValues); +CMSAPI void CMSEXPORT cmsFreeFloatArray(cmsFloatArray* v); + +// Observer and illuminant for a spectrally-based PCS (ICC.2:2023 Table 69). The +// observer step count N and the illuminant step count M are independent of each +// other and of the spectral PCS channel count -- nothing here may assume they agree. +typedef struct { + cmsContext ContextID; + + cmsUInt32Number ObserverType; // Table 70: 0 custom, 1 CIE 1931, 2 CIE 1964 + cmsFloat32Number ObserverStart; // nm + cmsFloat32Number ObserverEnd; // nm + cmsUInt16Number ObserverSteps; // N + cmsFloat32Number* Observer; // 3N values: all X, then all Y, then all Z + + cmsUInt32Number IlluminantType; // Table 71: 1 D50, 2 D65, 9 black body by CCT, ... + cmsFloat32Number CCT; // only meaningful for types 9 and 0Ah + cmsFloat32Number IlluminantStart; // nm + cmsFloat32Number IlluminantEnd; // nm + cmsUInt16Number IlluminantSteps; // M + cmsFloat32Number* Illuminant; // M values + + cmsCIEXYZ IlluminantXYZ; // un-normalised, Y in cd/m2 + cmsCIEXYZ SurroundXYZ; // un-normalised + +} cmsSpectralViewingConditions; + +CMSAPI cmsSpectralViewingConditions* + CMSEXPORT cmsAllocSpectralViewingConditions(cmsContext ContextID, + cmsUInt16Number ObserverSteps, + cmsUInt16Number IlluminantSteps); +CMSAPI void CMSEXPORT cmsFreeSpectralViewingConditions(cmsSpectralViewingConditions* v); + +// Read or write spectralWhitePointTag in any of the three encodings ICC.2 9.2.112 +// permits, including uInt16ArrayType, which has no registered type handler because +// its 1/65535 scaling belongs to this tag rather than to the generic ui16 signature. +// These dispatch on the tag's own type signature. On success the caller owns *Out +// and releases it with cmsFreeFloatArray. +CMSAPI cmsBool CMSEXPORT cmsReadSpectralWhitePoint(cmsHPROFILE hProfile, + cmsFloatArray** Out); +CMSAPI cmsBool CMSEXPORT cmsWriteSpectralWhitePoint(cmsHPROFILE hProfile, + const cmsFloatArray* In, + cmsTagTypeSignature AsType); +#endif + +#ifdef CMS_USE_ICCMAX_SPECTRAL +// Spectral PCS, from the ICC.2 (iccMAX) header. Zero when the profile does not +// declare one, which is the case for every ICC.1 profile. +CMSAPI cmsUInt32Number CMSEXPORT cmsGetSpectralPCS(cmsHPROFILE hProfile); +CMSAPI cmsBool CMSEXPORT cmsSetSpectralPCS(cmsHPROFILE hProfile, cmsUInt32Number SpectralPCS); +CMSAPI cmsUInt16Number CMSEXPORT cmsGetSpectralPCSChannels(cmsHPROFILE hProfile); +CMSAPI cmsBool CMSEXPORT cmsGetSpectralPCSRange(cmsHPROFILE hProfile, + cmsFloat32Number* Start, + cmsFloat32Number* End, + cmsUInt16Number* Steps); +CMSAPI cmsBool CMSEXPORT cmsSetSpectralPCSRange(cmsHPROFILE hProfile, + cmsFloat32Number Start, + cmsFloat32Number End, + cmsUInt16Number Steps); +#endif CMSAPI cmsColorSpaceSignature CMSEXPORT cmsGetColorSpace(cmsHPROFILE hProfile); CMSAPI void CMSEXPORT cmsSetColorSpace(cmsHPROFILE hProfile, cmsColorSpaceSignature sig); diff --git a/src/cmsgamma.c b/src/cmsgamma.c index 7cc2deb15..d89509da4 100644 --- a/src/cmsgamma.c +++ b/src/cmsgamma.c @@ -54,14 +54,39 @@ typedef struct _cmsParametricCurvesCollection_st { } _cmsParametricCurvesCollection; +#ifdef CMS_USE_ICCMAX_SPECTRAL + +// Exponentiation guarded against a non-positive base, which pow() would turn into +// NaN for a fractional exponent. Mirrors clipPow in the iccMAX reference +// implementation, and is used for every power in the ICC.2 formula segment types, +// including each X^gamma: a segmented curve's outer segments extend to +/-infinity, +// so a negative X is reachable. +static +cmsFloat64Number clipPow(cmsFloat64Number v, cmsFloat64Number g) +{ + if (v <= 0) return 0.0; + + return pow(v, g); +} + +#endif + // This is the default (built-in) evaluator static cmsFloat64Number DefaultEvalParametricFn(cmsInt32Number Type, const cmsFloat64Number Params[], cmsFloat64Number R); -// The built-in list +// The built-in list. The iccMAX types 9..13 are appended rather than inserted in +// numeric order: IsInSet returns a positional index that callers use against +// ParameterCount[], so existing positions must not shift. static _cmsParametricCurvesCollection DefaultCurves = { +#ifdef CMS_USE_ICCMAX_SPECTRAL + 15, // # of curve types + { 1, 2, 3, 4, 5, 6, 7, 8, 108, 109, 9, 10, 11, 12, 13 }, // Parametric curve ID + { 1, 3, 4, 5, 7, 4, 5, 5, 1, 1, 5, 5, 6, 7, 6 }, // Parameters by type +#else 10, // # of curve types { 1, 2, 3, 4, 5, 6, 7, 8, 108, 109 }, // Parametric curve ID { 1, 3, 4, 5, 7, 4, 5, 5, 1, 1 }, // Parameters by type +#endif DefaultEvalParametricFn, // Evaluator NULL // Next in chain }; @@ -228,6 +253,30 @@ cmsToneCurve* AllocateToneCurveStruct(cmsContext ContextID, cmsUInt32Number nEnt return NULL; } +#ifdef CMS_USE_ICCMAX_SPECTRAL + // IsInSet matches on abs(Type), so a negative type is accepted whenever the + // positive one is registered, on the understanding that the evaluator implements + // the analytic inverse. For the ICC.2 formula segment types 9..13 it deliberately + // does not, so a curve built with -9..-13 would evaluate to 0 everywhere. Reject + // it here -- the common path for both cmsBuildParametricToneCurve and + // cmsBuildSegmentedToneCurve -- rather than hand back a silently useless curve. + if (Segments != NULL) { + + for (i = 0; i < nSegments; i++) { + + if (Segments[i].Type <= -9 && Segments[i].Type >= -13) { + + cmsSignalError(ContextID, cmsERROR_RANGE, + "Parametric curve type %d has no analytic inverse in Little-CMS: the ICC.2 " + "formula segment types 9 to 13 are forward-only. Build the forward curve with " + "type %d and reverse it numerically with cmsReverseToneCurveEx.", + Segments[i].Type, -Segments[i].Type); + return NULL; + } + } + } +#endif // CMS_USE_ICCMAX_SPECTRAL + // Allocate all required pointers, etc. p = (cmsToneCurve*) _cmsMallocZero(ContextID, sizeof(cmsToneCurve)); if (!p) return NULL; @@ -685,6 +734,70 @@ cmsFloat64Number DefaultEvalParametricFn(cmsInt32Number Type, const cmsFloat64Nu break; +#ifdef CMS_USE_ICCMAX_SPECTRAL + + // ICC.2 formulaCurveSegment function types 0003h..0007h, which ICC.1 does not + // define. Parameter order follows ICC.2:2023 Table 111; note that omega precedes + // gamma in types 12 and 13. Degenerate cases return a finite value rather than + // NaN or an infinity, matching how types 6 and 7 above behave: a NaN reaching a + // CLUT index is far worse than a clamped number. The analytic inverses (-9..-13) + // are deliberately not implemented -- curve reversal in lcms is numerical, so no + // profile path needs them, and a caller asking for a negative type here falls + // through to the default arm and receives 0. + + // Y = a * (b * X + c)^g + d : g a b c d + case 9: + Val = Params[1] * clipPow(Params[2] * R + Params[3], Params[0]) + Params[4]; + break; + + // Y = a * ln(d * X^g - b) + c : g a b c d + case 10: + e = Params[4] * clipPow(R, Params[0]) - Params[2]; + if (e <= 0) + Val = Params[3]; + else + Val = Params[1] * log(e) + Params[3]; + break; + + // Y = e * exp((d * X^g - c) / a) + b : g a b c d e + case 11: + if (fabs(Params[1]) < MATRIX_DET_TOLERANCE) + Val = Params[2]; + else + Val = Params[5] * exp((Params[4] * clipPow(R, Params[0]) - Params[3]) / Params[1]) + Params[2]; + break; + + // Y = d * (max(e * X^g - a, 0) / (b - c * X^g))^w : w g a b c d e + case 12: + { + cmsFloat64Number u = clipPow(R, Params[1]); + cmsFloat64Number den = Params[3] - Params[4] * u; + cmsFloat64Number num = Params[6] * u - Params[2]; + + if (num < 0) num = 0; + + if (fabs(den) < MATRIX_DET_TOLERANCE) + Val = 0; + else + Val = Params[5] * clipPow(num / den, Params[0]); + } + break; + + // Y = d * ((a + b * X^g) / (1 + c * X^g))^w : w g a b c d + case 13: + { + cmsFloat64Number u = clipPow(R, Params[1]); + cmsFloat64Number den = 1.0 + Params[4] * u; + + if (fabs(den) < MATRIX_DET_TOLERANCE) + Val = 0; + else + Val = Params[5] * clipPow((Params[2] + Params[3] * u) / den, Params[0]); + } + break; + +#endif + // S-Shaped: (1 - (1-x)^1/g)^1/g case 108: if (fabs(Params[0]) < MATRIX_DET_TOLERANCE) @@ -967,9 +1080,19 @@ void CMSEXPORT cmsFreeToneCurveTriple(cmsToneCurve* Curve[3]) // Duplicate a gamma table cmsToneCurve* CMSEXPORT cmsDupToneCurve(const cmsToneCurve* In) { + cmsToneCurve* Out; + if (In == NULL) return NULL; - return AllocateToneCurveStruct(In ->InterpParams ->ContextID, In ->nEntries, In ->nSegments, In ->Segments, In ->Table16); + Out = AllocateToneCurveStruct(In ->InterpParams ->ContextID, In ->nEntries, In ->nSegments, In ->Segments, In ->Table16); + +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Carry the encoding across, so a duplicated curve is still written back in the + // form it arrived in. AllocateToneCurveStruct does not know about it. + if (Out != NULL) Out ->CurveType = In ->CurveType; +#endif + + return Out; } // Joins two curves for X and Y. Curves should be monotonic. diff --git a/src/cmsio0.c b/src/cmsio0.c index b64f9328c..a19c21610 100644 --- a/src/cmsio0.c +++ b/src/cmsio0.c @@ -902,7 +902,16 @@ cmsBool _cmsReadHeader(_cmsICCPROFILE* Icc) _cmsAdjustEndianess64(&Icc -> attributes, &Header.attributes); Icc -> Version = _cmsAdjustEndianess32(_validatedVersion(Header.version)); +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Gate on the major version only. The version field is binary coded decimal with + // the major version in byte 8, and per ICC.2:2023 7.2.6 a minor version change + // happens precisely when profiles conforming to the revised spec can still be + // processed by existing CMMs. Real iccMAX profiles declare 5.1, so a test against + // the whole word would turn them away for no reason. + if ((Icc->Version >> 24) > 0x05) { +#else if (Icc->Version > 0x5000000) { +#endif cmsSignalError(Icc->ContextID, cmsERROR_UNKNOWN_EXTENSION, "Unsupported profile version '0x%x'", Icc->Version); return FALSE; } @@ -926,6 +935,36 @@ cmsBool _cmsReadHeader(_cmsICCPROFILE* Icc) // The profile ID are 32 raw bytes memmove(Icc ->ProfileID.ID32, Header.profileID.ID32, 16); +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Pick up the spectral PCS, which lives in what ICC.1 calls the reserved area. + // reserved[0] is header byte 100, since profileID ends at byte 99. So the + // spectral PCS signature (bytes 100-103) is reserved[0..3] and the spectral + // range (bytes 104-109) is reserved[4..9]. Do not confuse these with the MCS + // signature at bytes 116-119, which is reserved[16..19]. + // + // Only for v5 and above. ICC.1 says that area shall be zero but nothing enforces + // it, so junk in a v2 or v4 profile must not be reported as a spectral PCS. + if ((Icc ->Version >> 24) >= 0x05) { + + cmsUInt32Number SpectralPCS; + cmsUInt16Number Start, End, Steps; + + memmove(&SpectralPCS, Header.reserved + 0, 4); + Icc ->SpectralPCS = _cmsAdjustEndianess32(SpectralPCS); + + if (Icc ->SpectralPCS != 0) { + + memmove(&Start, Header.reserved + 4, 2); + memmove(&End, Header.reserved + 6, 2); + memmove(&Steps, Header.reserved + 8, 2); + + Icc ->SpectralPCSStart = _cmsHalf2Float(_cmsAdjustEndianess16(Start)); + Icc ->SpectralPCSEnd = _cmsHalf2Float(_cmsAdjustEndianess16(End)); + Icc ->SpectralPCSSteps = _cmsAdjustEndianess16(Steps); + } + } +#endif // CMS_USE_ICCMAX_SPECTRAL + // Read tag directory if (!_cmsReadUInt32Number(io, &TagCount)) return FALSE; @@ -1040,6 +1079,28 @@ cmsBool _cmsWriteHeader(_cmsICCPROFILE* Icc, cmsUInt32Number UsedSpace) memset(&Header.reserved, 0, sizeof(Header.reserved)); +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Put the spectral PCS back where it came from, at header bytes 100-109. + // reserved[0] is byte 100. Everything else in the reserved area stays zero, + // so a v5 profile using the bi-spectral range, MCS or sub-class fields does + // not survive a round trip through here. + // + // Only for v5 and above, mirroring the read side: those bytes are reserved in + // ICC.1, so nothing may be authored there in a v2 or v4 profile. + if (Icc ->SpectralPCS != 0 && (Icc ->Version >> 24) >= 0x05) { + + cmsUInt32Number SpectralPCS = _cmsAdjustEndianess32(Icc ->SpectralPCS); + cmsUInt16Number Steps = _cmsAdjustEndianess16(Icc ->SpectralPCSSteps); + cmsUInt16Number Start = _cmsAdjustEndianess16(_cmsFloat2Half(Icc ->SpectralPCSStart)); + cmsUInt16Number End = _cmsAdjustEndianess16(_cmsFloat2Half(Icc ->SpectralPCSEnd)); + + memmove(Header.reserved + 0, &SpectralPCS, 4); + memmove(Header.reserved + 4, &Start, 2); + memmove(Header.reserved + 6, &End, 2); + memmove(Header.reserved + 8, &Steps, 2); + } +#endif // CMS_USE_ICCMAX_SPECTRAL + // Set profile ID. Endianness is always big endian memmove(&Header.profileID, &Icc ->ProfileID, 16); @@ -1183,6 +1244,311 @@ void CMSEXPORT cmsSetPCS(cmsHPROFILE hProfile, cmsColorSpaceSignature pcs) Icc -> PCS = pcs; } +#ifdef CMS_USE_ICCMAX_SPECTRAL +// The spectral PCS signature, from header bytes 100-103. Zero means the profile +// does not use a spectrally-based PCS, which is always the case for ICC.1. +cmsUInt32Number CMSEXPORT cmsGetSpectralPCS(cmsHPROFILE hProfile) +{ + _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; + return Icc -> SpectralPCS; +} + +// The spectral PCS is an ICC.2-only header field, so refuse to author one into a +// profile that does not declare at least version 5. Set the version first. Returns +// FALSE when the version gate rejects the call, so the caller can tell, rather than +// having to read the field back to find out -- mirroring cmsSetSpectralPCSRange. +cmsBool CMSEXPORT cmsSetSpectralPCS(cmsHPROFILE hProfile, cmsUInt32Number SpectralPCS) +{ + _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; + + if ((Icc ->Version >> 24) < 0x05) return FALSE; + + Icc -> SpectralPCS = SpectralPCS; + + return TRUE; +} + +// Spectral colour space signatures pack a two character type identifier in the high +// 16 bits and the channel count in the low 16 bits, e.g. 'rs' 0025h is reflectance +// with 37 channels (ICC.2:2023 Table 21). Note that nothing here checks the count +// against the steps field of the spectral range, which the spec requires to match. +cmsUInt16Number CMSEXPORT cmsGetSpectralPCSChannels(cmsHPROFILE hProfile) +{ + _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; + + if (Icc -> SpectralPCS == 0) return 0; + + return (cmsUInt16Number) (Icc -> SpectralPCS & 0xFFFF); +} + +cmsBool CMSEXPORT cmsGetSpectralPCSRange(cmsHPROFILE hProfile, + cmsFloat32Number* Start, + cmsFloat32Number* End, + cmsUInt16Number* Steps) +{ + _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; + + if (Icc -> SpectralPCS == 0) return FALSE; + + if (Start != NULL) *Start = Icc -> SpectralPCSStart; + if (End != NULL) *End = Icc -> SpectralPCSEnd; + if (Steps != NULL) *Steps = Icc -> SpectralPCSSteps; + + return TRUE; +} + +cmsBool CMSEXPORT cmsSetSpectralPCSRange(cmsHPROFILE hProfile, + cmsFloat32Number Start, + cmsFloat32Number End, + cmsUInt16Number Steps) +{ + _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; + + // ICC.2-only header field, so the profile has to declare at least version 5 + if ((Icc ->Version >> 24) < 0x05) return FALSE; + + // The range is only meaningful alongside a spectral PCS signature, and the + // spec requires it to be zero when that signature is zero + if (Icc -> SpectralPCS == 0) return FALSE; + + Icc -> SpectralPCSStart = Start; + Icc -> SpectralPCSEnd = End; + Icc -> SpectralPCSSteps = Steps; + + return TRUE; +} + +// Allocates a spectral value array, zeroed, defaulting to the lossless encoding. +// The upper bound is the largest channel count an ICC.2 spectral PCS signature can +// express, since its channel count is a 16 bit field (ICC.2 Table 21). +cmsFloatArray* CMSEXPORT cmsAllocFloatArray(cmsContext ContextID, cmsUInt32Number nValues) +{ + cmsFloatArray* v; + + if (nValues == 0 || nValues > 0xFFFF) return NULL; + + v = (cmsFloatArray*) _cmsMallocZero(ContextID, sizeof(cmsFloatArray)); + if (v == NULL) return NULL; + + // Set ContextID before anything can fail, so every free path uses the same + // allocator the allocation came from + v ->ContextID = ContextID; + + v ->Values = (cmsFloat32Number*) _cmsCalloc(ContextID, nValues, sizeof(cmsFloat32Number)); + if (v ->Values == NULL) { + + _cmsFree(ContextID, v); + return NULL; + } + + v ->nValues = nValues; + + return v; +} + +void CMSEXPORT cmsFreeFloatArray(cmsFloatArray* v) +{ + if (v == NULL) return; + + if (v ->Values != NULL) _cmsFree(v ->ContextID, v ->Values); + + _cmsFree(v ->ContextID, v); +} + +void CMSEXPORT cmsFreeSpectralViewingConditions(cmsSpectralViewingConditions* v) +{ + if (v == NULL) return; + + if (v ->Observer != NULL) _cmsFree(v ->ContextID, v ->Observer); + if (v ->Illuminant != NULL) _cmsFree(v ->ContextID, v ->Illuminant); + + _cmsFree(v ->ContextID, v); +} + +// Observer and illuminant step counts are independent of each other and of the +// spectral PCS channel count (ICC.2:2023 Table 69) -- nothing here assumes they agree. +cmsSpectralViewingConditions* CMSEXPORT cmsAllocSpectralViewingConditions(cmsContext ContextID, + cmsUInt16Number ObserverSteps, + cmsUInt16Number IlluminantSteps) +{ + cmsSpectralViewingConditions* v; + + if (ObserverSteps == 0 || IlluminantSteps == 0) return NULL; + + v = (cmsSpectralViewingConditions*) _cmsMallocZero(ContextID, sizeof(cmsSpectralViewingConditions)); + if (v == NULL) return NULL; + + // Set ContextID first, so the partial-failure path below frees against the same + // allocator the allocations came from + v ->ContextID = ContextID; + + // 3N for the observer: the X vector, then Y, then Z + v ->Observer = (cmsFloat32Number*) _cmsCalloc(ContextID, 3 * (cmsUInt32Number) ObserverSteps, + sizeof(cmsFloat32Number)); + v ->Illuminant = (cmsFloat32Number*) _cmsCalloc(ContextID, IlluminantSteps, + sizeof(cmsFloat32Number)); + + if (v ->Observer == NULL || v ->Illuminant == NULL) { + + cmsFreeSpectralViewingConditions(v); + return NULL; + } + + v ->ObserverSteps = ObserverSteps; + v ->IlluminantSteps = IlluminantSteps; + + return v; +} + +cmsBool CMSEXPORT cmsReadSpectralWhitePoint(cmsHPROFILE hProfile, cmsFloatArray** Out) +{ + cmsUInt8Number* Raw = NULL; + cmsFloatArray* v = NULL; + cmsUInt32Number Size, Type, n, i, BytesPerValue; + + if (Out == NULL) return FALSE; + *Out = NULL; + + Size = cmsReadRawTag(hProfile, cmsSigSpectralWhitePointTag, NULL, 0); + if (Size < 12) return FALSE; // 8 byte prefix plus at least one value + + Raw = (cmsUInt8Number*) _cmsMalloc(cmsGetProfileContextID(hProfile), Size); + if (Raw == NULL) return FALSE; + + if (cmsReadRawTag(hProfile, cmsSigSpectralWhitePointTag, Raw, Size) != Size) goto Error; + + // Bytes 0..3 are the type signature, 4..7 reserved, the values follow + Type = _cmsAdjustEndianess32(*(cmsUInt32Number*) Raw); + + switch (Type) { + + case cmsSigFloat32ArrayType: BytesPerValue = 4; break; + case cmsSigFloat16ArrayType: BytesPerValue = 2; break; + case cmsSigUInt16ArrayType: BytesPerValue = 2; break; + + default: + cmsSignalError(cmsGetProfileContextID(hProfile), cmsERROR_UNKNOWN_EXTENSION, + "swpt has type '%x', which ICC.2 9.2.112 does not permit", Type); + goto Error; + } + + n = (Size - 8) / BytesPerValue; + if (n == 0) goto Error; + + v = cmsAllocFloatArray(cmsGetProfileContextID(hProfile), n); + if (v == NULL) goto Error; + + for (i = 0; i < n; i++) { + + cmsUInt8Number* p = Raw + 8 + i * BytesPerValue; + + if (Type == cmsSigFloat32ArrayType) { + + cmsUInt32Number bits = _cmsAdjustEndianess32(*(cmsUInt32Number*) p); + memcpy(&v ->Values[i], &bits, sizeof(cmsFloat32Number)); + } + else if (Type == cmsSigFloat16ArrayType) { + + v ->Values[i] = _cmsHalf2Float(_cmsAdjustEndianess16(*(cmsUInt16Number*) p)); + } + else { + + // ui16 is 0 to 65535 mapped onto 0,0 to 1,0, per icU16toF in the + // reference implementation. ICC.2 itself does not state this. + v ->Values[i] = (cmsFloat32Number) + (_cmsAdjustEndianess16(*(cmsUInt16Number*) p) / 65535.0); + } + } + + // This accessor no longer reports which encoding it found; a caller that + // needs to know can inspect the first four bytes of cmsReadRawTag itself. + + _cmsFree(cmsGetProfileContextID(hProfile), Raw); + *Out = v; + return TRUE; + +Error: + if (v != NULL) cmsFreeFloatArray(v); + if (Raw != NULL) _cmsFree(cmsGetProfileContextID(hProfile), Raw); + return FALSE; +} + +cmsBool CMSEXPORT cmsWriteSpectralWhitePoint(cmsHPROFILE hProfile, + const cmsFloatArray* In, + cmsTagTypeSignature AsType) +{ + cmsUInt8Number* Raw = NULL; + cmsUInt32Number Size, i, BytesPerValue; + cmsBool rc; + + if (In == NULL || In ->Values == NULL || In ->nValues == 0) return FALSE; + + // cmsAllocFloatArray caps nValues at 0xFFFF -- the largest channel count an + // ICC.2 spectral PCS signature can express, since its channel count is a 16 bit + // field -- but cmsFloatArray is a public struct and nothing stops a caller from + // populating one by hand and setting nValues past that bound. Without this + // guard, "8 + In->nValues * BytesPerValue" below can wrap a 32 bit Size to a + // tiny value (e.g. nValues = 0x40000000, BytesPerValue = 4, product 0x100000000 + // wraps to 0, Size becomes 8), so the allocation would succeed far too small and + // the write loop would then walk off the end of it. + if (In ->nValues > 0xFFFF) return FALSE; + + switch (AsType) { + + case cmsSigFloat32ArrayType: BytesPerValue = 4; break; + case cmsSigFloat16ArrayType: BytesPerValue = 2; break; + case cmsSigUInt16ArrayType: BytesPerValue = 2; break; + + default: + cmsSignalError(cmsGetProfileContextID(hProfile), cmsERROR_UNKNOWN_EXTENSION, + "ICC.2 9.2.112 does not permit type '%x' for swpt", AsType); + return FALSE; + } + + Size = 8 + In ->nValues * BytesPerValue; + + Raw = (cmsUInt8Number*) _cmsMallocZero(cmsGetProfileContextID(hProfile), Size); + if (Raw == NULL) return FALSE; + + *(cmsUInt32Number*) Raw = _cmsAdjustEndianess32((cmsUInt32Number) AsType); + // Bytes 4..7 stay zero: ICC.2 requires the reserved field to be 0 + + for (i = 0; i < In ->nValues; i++) { + + cmsUInt8Number* p = Raw + 8 + i * BytesPerValue; + + if (AsType == cmsSigFloat32ArrayType) { + + cmsUInt32Number bits; + memcpy(&bits, &In ->Values[i], sizeof(cmsUInt32Number)); + *(cmsUInt32Number*) p = _cmsAdjustEndianess32(bits); + } + else if (AsType == cmsSigFloat16ArrayType) { + + *(cmsUInt16Number*) p = _cmsAdjustEndianess16(_cmsFloat2Half(In ->Values[i])); + } + else { + + // Mirrors icFtoU16: infinity becomes 1,0, NaN becomes 0, the value is + // clamped to 0,0 .. 1,0, then scaled and rounded + cmsFloat64Number x = In ->Values[i]; + + if (isnan(x)) x = 0.0; + else if (x > 1.0) x = 1.0; // also catches +infinity + else if (x < 0.0) x = 0.0; // also catches -infinity + + *(cmsUInt16Number*) p = + _cmsAdjustEndianess16((cmsUInt16Number) floor(x * 65535.0 + 0.5)); + } + } + + rc = cmsWriteRawTag(hProfile, cmsSigSpectralWhitePointTag, Raw, Size); + + _cmsFree(cmsGetProfileContextID(hProfile), Raw); + return rc; +} +#endif // CMS_USE_ICCMAX_SPECTRAL + cmsColorSpaceSignature CMSEXPORT cmsGetColorSpace(cmsHPROFILE hProfile) { _cmsICCPROFILE* Icc = (_cmsICCPROFILE*) hProfile; diff --git a/src/cmslut.c b/src/cmslut.c index f34496cbb..3662e8135 100644 --- a/src/cmslut.c +++ b/src/cmslut.c @@ -551,7 +551,7 @@ cmsStage* CMSEXPORT cmsStageAllocCLut16bitGranular(cmsContext ContextID, cmsUInt32Number outputChan, const cmsUInt16Number* Table) { - cmsUInt32Number i, n; + cmsUInt32Number i, n, cube; _cmsStageCLutData* NewElem; cmsStage* NewMPE; @@ -575,14 +575,20 @@ cmsStage* CMSEXPORT cmsStageAllocCLut16bitGranular(cmsContext ContextID, NewMPE ->Data = (void*) NewElem; - NewElem -> nEntries = n = outputChan * CubeSize(clutPoints, inputChan); - NewElem -> HasFloatValues = FALSE; + // There is a potential integer overflow on computing n and nEntries. CubeSize only + // bounds its result against a fixed divisor, so the multiply by outputChan has to + // be checked here: a table whose product does not fit in 32 bits would be allocated + // short while _cmsComputeInterpParamsEx computed full-width strides for it. + cube = CubeSize(clutPoints, inputChan); - if (n == 0) { + if (cube == 0 || outputChan == 0 || cube > UINT_MAX / outputChan) { cmsStageFree(NewMPE); return NULL; } + NewElem -> nEntries = n = outputChan * cube; + NewElem -> HasFloatValues = FALSE; + NewElem ->Tab.T = (cmsUInt16Number*) _cmsCalloc(ContextID, n, sizeof(cmsUInt16Number)); if (NewElem ->Tab.T == NULL) { @@ -642,7 +648,7 @@ cmsStage* CMSEXPORT cmsStageAllocCLutFloat(cmsContext ContextID, cmsStage* CMSEXPORT cmsStageAllocCLutFloatGranular(cmsContext ContextID, const cmsUInt32Number clutPoints[], cmsUInt32Number inputChan, cmsUInt32Number outputChan, const cmsFloat32Number* Table) { - cmsUInt32Number i, n; + cmsUInt32Number i, n, cube; _cmsStageCLutData* NewElem; cmsStage* NewMPE; @@ -666,15 +672,20 @@ cmsStage* CMSEXPORT cmsStageAllocCLutFloatGranular(cmsContext ContextID, const c NewMPE ->Data = (void*) NewElem; - // There is a potential integer overflow on conputing n and nEntries. - NewElem -> nEntries = n = outputChan * CubeSize(clutPoints, inputChan); - NewElem -> HasFloatValues = TRUE; + // There is a potential integer overflow on computing n and nEntries. CubeSize only + // bounds its result against a fixed divisor, so the multiply by outputChan has to + // be checked here: a table whose product does not fit in 32 bits would be allocated + // short while _cmsComputeInterpParamsEx computed full-width strides for it. + cube = CubeSize(clutPoints, inputChan); - if (n == 0) { + if (cube == 0 || outputChan == 0 || cube > UINT_MAX / outputChan) { cmsStageFree(NewMPE); return NULL; } + NewElem -> nEntries = n = outputChan * cube; + NewElem -> HasFloatValues = TRUE; + NewElem ->Tab.TFloat = (cmsFloat32Number*) _cmsCalloc(ContextID, n, sizeof(cmsFloat32Number)); if (NewElem ->Tab.TFloat == NULL) { cmsStageFree(NewMPE); @@ -1380,8 +1391,18 @@ cmsPipeline* CMSEXPORT cmsPipelineAlloc(cmsContext ContextID, cmsUInt32Number In cmsPipeline* NewLUT; // A value of zero in channels is allowed as placeholder +#ifdef CMS_USE_ICCMAX_SPECTRAL + // The limit is MAX_STAGE_CHANNELS rather than cmsMAXCHANNELS so that MPE + // pipelines can carry an iccMAX spectral PCS (37+ channels). The float and + // 16-bit eval paths both use MAX_STAGE_CHANNELS buffers. Note this ceiling + // does NOT apply to transforms: cmsxform.c still uses cmsMAXCHANNELS-sized + // buffers, so such a pipeline must only be evaluated directly. + if (InputChannels >= MAX_STAGE_CHANNELS || + OutputChannels >= MAX_STAGE_CHANNELS) return NULL; +#else if (InputChannels >= cmsMAXCHANNELS || OutputChannels >= cmsMAXCHANNELS) return NULL; +#endif NewLUT = (cmsPipeline*) _cmsMallocZero(ContextID, sizeof(cmsPipeline)); if (NewLUT == NULL) return NULL; diff --git a/src/cmsplugin.c b/src/cmsplugin.c index 21465f38a..7882b431b 100644 --- a/src/cmsplugin.c +++ b/src/cmsplugin.c @@ -321,6 +321,43 @@ cmsBool CMSEXPORT _cmsWriteFloat32Number(cmsIOHANDLER* io, cmsFloat32Number n) return TRUE; } +#ifndef CMS_NO_HALF_SUPPORT +#ifdef CMS_USE_ICCMAX_SPECTRAL + +// Reads a float16Number (IEEE 754 half precision), widened to float32. Used by +// the iccMAX spectralRange, extendedCLUTElement and singleSampledCurve encodings. +// The conversion itself lives in cmshalf.c; this only handles IO and byte order. +cmsBool CMSEXPORT _cmsReadFloat16Number(cmsIOHANDLER* io, cmsFloat32Number* n) +{ + cmsUInt16Number tmp; + + _cmsAssert(io != NULL); + + if (io->Read(io, &tmp, sizeof(cmsUInt16Number), 1) != 1) + return FALSE; + + if (n != NULL) *n = _cmsHalf2Float(_cmsAdjustEndianess16(tmp)); + + return TRUE; +} + +// Writes a float32 as a float16Number (IEEE 754 half precision) +cmsBool CMSEXPORT _cmsWriteFloat16Number(cmsIOHANDLER* io, cmsFloat32Number n) +{ + cmsUInt16Number tmp; + + _cmsAssert(io != NULL); + + tmp = _cmsAdjustEndianess16(_cmsFloat2Half(n)); + if (io -> Write(io, sizeof(cmsUInt16Number), &tmp) != 1) + return FALSE; + + return TRUE; +} + +#endif // CMS_USE_ICCMAX_SPECTRAL +#endif // CMS_NO_HALF_SUPPORT + cmsBool CMSEXPORT _cmsWriteUInt64Number(cmsIOHANDLER* io, cmsUInt64Number* n) { cmsUInt64Number tmp; diff --git a/src/cmstypes.c b/src/cmstypes.c index 5fe743484..119930862 100644 --- a/src/cmstypes.c +++ b/src/cmstypes.c @@ -4238,7 +4238,8 @@ void GenericMPEfree(struct _cms_typehandler_struct* self, void *Ptr) // specified either in terms of a formula, or by a sampled curve. -// Read an embedded segmented curve +// Read an embedded segmented curve. The type signature has already been consumed by +// ReadMPEEmbeddedCurve, which dispatched here. static cmsToneCurve* ReadSegmentedCurve(struct _cms_typehandler_struct* self, cmsIOHANDLER* io) { @@ -4249,12 +4250,6 @@ cmsToneCurve* ReadSegmentedCurve(struct _cms_typehandler_struct* self, cmsIOHAND cmsToneCurve* Curve; cmsFloat32Number PrevBreak = MINUS_INF; // - infinite - // Take signature and channels for each element. - if (!_cmsReadUInt32Number(io, (cmsUInt32Number*) &ElementSig)) return NULL; - - // That should be a segmented curve - if (ElementSig != cmsSigSegmentedCurve) return NULL; - if (!_cmsReadUInt32Number(io, NULL)) return NULL; if (!_cmsReadUInt16Number(io, &nSegments)) return NULL; if (!_cmsReadUInt16Number(io, NULL)) return NULL; @@ -4284,14 +4279,23 @@ cmsToneCurve* ReadSegmentedCurve(struct _cms_typehandler_struct* self, cmsIOHAND case cmsSigFormulaCurveSeg: { +#ifdef CMS_USE_ICCMAX_SPECTRAL + // ICC.2 Table 111 defines eight function types; ICC.1 defines only the + // first three. Parameter counts per CIccFormulaCurveSegment::Read. + cmsUInt16Number Type; + cmsUInt32Number ParamsByType[] = { 4, 5, 5, 5, 5, 6, 7, 6 }; + cmsUInt16Number MaxType = 7; +#else cmsUInt16Number Type; cmsUInt32Number ParamsByType[] = { 4, 5, 5 }; + cmsUInt16Number MaxType = 2; +#endif if (!_cmsReadUInt16Number(io, &Type)) goto Error; if (!_cmsReadUInt16Number(io, NULL)) goto Error; Segments[i].Type = Type + 6; - if (Type > 2) goto Error; + if (Type > MaxType) goto Error; for (j = 0; j < ParamsByType[Type]; j++) { @@ -4363,6 +4367,266 @@ cmsToneCurve* ReadSegmentedCurve(struct _cms_typehandler_struct* self, cmsIOHAND } +#ifdef CMS_USE_ICCMAX_SPECTRAL +// Read an embedded singleSampledCurve (iccMAX 'sngf', ICC.2:2023 11.2.2.2, Table 108). +// The type signature has already been consumed by ReadMPEEmbeddedCurve. +// +// 0..3 'sngf' signature ] consumed by the caller +// 4..7 reserved, shall be 0 +// 8..11 number of entries (N) uInt32Number, at least 2 +// 12..15 input of first entry(F) float32Number +// 16..19 input of last entry (L) float32Number, greater than F +// 20..21 lookup extension type uInt16Number, 0 clips and 1 extrapolates +// 22..23 data encoding type uInt16Number as valueEncodingType +// 24.. N entries per the encoding type +// +// This maps onto a three segment cmsToneCurve: the middle segment is the sampled data +// over [F, L], and the outer two implement the extension behaviour as type 6 formulas +// with Gamma 1, which cmsgamma.c evaluates as a plain unclamped line. The formulas +// match CIccSingleSampledCurve::Begin in the reference implementation, and are +// continuous with the sampled segment at F and L, so it does not matter which side of +// the boundary EvalSegmentedFn happens to pick. +static +cmsToneCurve* ReadSingleSampledCurve(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number SizeOfTag) +{ + cmsUInt32Number nEntries, i, BytesPerSample; + cmsFloat32Number FirstEntry, LastEntry, StepSize, Slope; + cmsUInt16Number ExtensionType, EncodingType; + cmsFloat32Number* SampledPoints = NULL; + cmsCurveSegment Seg[3]; + cmsToneCurve* Curve; + + // Bytes consumed before the samples start: the 'sngf' signature, read by + // ReadMPEEmbeddedCurve, plus Table 108's own 20 byte header read below. +#define SNGF_HEADER_BYTES 24 + + if (!_cmsReadUInt32Number(io, NULL)) return NULL; // reserved + + if (!_cmsReadUInt32Number(io, &nEntries)) return NULL; + + // At least two entries, and keep the allocation sane + if (nEntries < 2) return NULL; + if (nEntries > 0x10000) return NULL; + + if (!_cmsReadFloat32Number(io, &FirstEntry)) return NULL; + if (!_cmsReadFloat32Number(io, &LastEntry)) return NULL; + + // F shall be less than L. This also rejects a NaN in either, since every + // comparison against NaN is false. + if (!(LastEntry > FirstEntry)) return NULL; + + // An infinite endpoint would make StepSize infinite and the extension slopes zero, + // which is not a curve anyone can have meant + if (isinf(FirstEntry) || isinf(LastEntry)) { + + cmsSignalError(self ->ContextID, cmsERROR_RANGE, + "singleSampledCurve has a non-finite endpoint"); + return NULL; + } + + if (!_cmsReadUInt16Number(io, &ExtensionType)) return NULL; + if (!_cmsReadUInt16Number(io, &EncodingType)) return NULL; + + if (ExtensionType > 1) { + + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, + "Unknown singleSampledCurve lookup extension type '%d'", ExtensionType); + return NULL; + } + + // How many bytes each sample takes, per ICC.2:2023 Table 8 + switch (EncodingType) { + + case 0: BytesPerSample = 4; break; // float32Number + case 1: BytesPerSample = 2; break; // float16Number + case 2: BytesPerSample = 2; break; // uInt16Number + case 3: BytesPerSample = 1; break; // uInt8Number + + default: + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, + "Unknown singleSampledCurve value encoding type '%d'", EncodingType); + return NULL; + } + + // The samples the header declares must fit in the bytes the position table gave + // this curve, otherwise the reads below would run off the end of the profile + if (SizeOfTag < SNGF_HEADER_BYTES) return NULL; + if (nEntries > (SizeOfTag - SNGF_HEADER_BYTES) / BytesPerSample) return NULL; + + SampledPoints = (cmsFloat32Number*) _cmsCalloc(self ->ContextID, nEntries, sizeof(cmsFloat32Number)); + if (SampledPoints == NULL) return NULL; + + switch (EncodingType) { + + case 0: // float32Number + for (i = 0; i < nEntries; i++) { + + if (!_cmsReadFloat32Number(io, &SampledPoints[i])) goto Error; + } + break; + + case 1: // float16Number + for (i = 0; i < nEntries; i++) { + + if (!_cmsReadFloat16Number(io, &SampledPoints[i])) goto Error; + } + break; + + case 2: // uInt16Number, encoding 0.0 to 1.0 + for (i = 0; i < nEntries; i++) { + + cmsUInt16Number v; + + if (!_cmsReadUInt16Number(io, &v)) goto Error; + SampledPoints[i] = (cmsFloat32Number) v / 65535.0f; + } + break; + + case 3: // uInt8Number, encoding 0.0 to 1.0 + for (i = 0; i < nEntries; i++) { + + cmsUInt8Number v; + + if (!_cmsReadUInt8Number(io, &v)) goto Error; + SampledPoints[i] = (cmsFloat32Number) v / 255.0f; + } + break; + + default: + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, + "Unknown singleSampledCurve value encoding type '%d'", EncodingType); + goto Error; + } + + memset(Seg, 0, sizeof(Seg)); + + StepSize = (LastEntry - FirstEntry) / (cmsFloat32Number) (nEntries - 1); + + // L > F and both are finite, yet the quotient can still underflow to zero for a + // denormal L - F divided by a large nEntries. The extension slopes below divide by + // StepSize, so a zero would make them infinite -- or NaN, when the two end samples + // are equal and the division is 0/0 -- and cmsEvalToneCurveFloat would then hand + // back NaN for any input outside [F, L]. + if (!(StepSize > 0.0f)) { + + cmsSignalError(self ->ContextID, cmsERROR_RANGE, + "singleSampledCurve step size underflowed to zero: %u entries over [%g, %g]", + nEntries, (cmsFloat64Number) FirstEntry, (cmsFloat64Number) LastEntry); + goto Error; + } + + // Below F + Seg[0].x0 = MINUS_INF; + Seg[0].x1 = FirstEntry; + Seg[0].Type = 6; + Seg[0].Params[0] = 1.0; // Gamma 1, so this is just a*X + b + c + + if (ExtensionType == 0) { + + // Clip to the first entry + Seg[0].Params[1] = 0.0; + Seg[0].Params[2] = 0.0; + Seg[0].Params[3] = SampledPoints[0]; + } + else { + // Extrapolate along the line through the first two entries + Slope = (SampledPoints[1] - SampledPoints[0]) / StepSize; + + Seg[0].Params[1] = Slope; + Seg[0].Params[2] = SampledPoints[0] - Slope * FirstEntry; + Seg[0].Params[3] = 0.0; + } + + // The sampled data itself, over [F, L] + Seg[1].x0 = FirstEntry; + Seg[1].x1 = LastEntry; + Seg[1].Type = 0; + Seg[1].nGridPoints = nEntries; + Seg[1].SampledPoints = SampledPoints; + + // Above L + Seg[2].x0 = LastEntry; + Seg[2].x1 = PLUS_INF; + Seg[2].Type = 6; + Seg[2].Params[0] = 1.0; + + if (ExtensionType == 0) { + + Seg[2].Params[1] = 0.0; + Seg[2].Params[2] = 0.0; + Seg[2].Params[3] = SampledPoints[nEntries - 1]; + } + else { + Slope = (SampledPoints[nEntries - 1] - SampledPoints[nEntries - 2]) / StepSize; + + Seg[2].Params[1] = Slope; + Seg[2].Params[2] = SampledPoints[nEntries - 1] - Slope * LastEntry; + Seg[2].Params[3] = 0.0; + } + + // cmsBuildSegmentedToneCurve duplicates the sampled points, so ours can go + Curve = cmsBuildSegmentedToneCurve(self ->ContextID, 3, Seg); + _cmsFree(self ->ContextID, SampledPoints); + + if (Curve == NULL) return NULL; + + Curve ->CurveType = cmsSigSingleSampledCurve; + + return Curve; + +Error: + if (SampledPoints != NULL) _cmsFree(self ->ContextID, SampledPoints); + return NULL; +} + +#undef SNGF_HEADER_BYTES +#endif // CMS_USE_ICCMAX_SPECTRAL + + +// Read one curve of a curveSetElement, dispatching on its type signature. ICC.1 only +// defines the segmentedCurve; ICC.2 adds singleSampledCurve and, not supported here, +// sampledCalculatorCurve. Not to be confused with ReadEmbeddedCurve above, which reads +// the wholly different curveType/parametricCurveType pair used by lut8/lut16/mAB/mBA. +static +cmsToneCurve* ReadMPEEmbeddedCurve(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number SizeOfTag) +{ + cmsCurveSegSignature CurveSig; + +#ifndef CMS_USE_ICCMAX_SPECTRAL + // Only ReadSingleSampledCurve needs the size, and it is not compiled in + cmsUNUSED_PARAMETER(SizeOfTag); +#endif + + if (!_cmsReadUInt32Number(io, (cmsUInt32Number*) &CurveSig)) return NULL; + + switch (CurveSig) { + + case cmsSigSegmentedCurve: + return ReadSegmentedCurve(self, io); + +#ifdef CMS_USE_ICCMAX_SPECTRAL + case cmsSigSingleSampledCurve: + return ReadSingleSampledCurve(self, io, SizeOfTag); + + default: + { + char String[5]; + + _cmsTagSignature2String(String, (cmsTagSignature) CurveSig); + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, "Unknown MPE curve type '%s'", String); + } + return NULL; +#else + default: + // Master returned NULL here without signalling. Signalling would turn a + // failed tag read into exit(1) under a fatal error handler, so the quiet + // behaviour is preserved when the feature is off. + return NULL; +#endif + } +} + + static cmsBool ReadMPECurve(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, @@ -4372,10 +4636,9 @@ cmsBool ReadMPECurve(struct _cms_typehandler_struct* self, { cmsToneCurve** GammaTables = ( cmsToneCurve**) Cargo; - GammaTables[n] = ReadSegmentedCurve(self, io); + // SizeOfTag is this curve's own size, from the curveSetElement position table + GammaTables[n] = ReadMPEEmbeddedCurve(self, io, SizeOfTag); return (GammaTables[n] != NULL); - - cmsUNUSED_PARAMETER(SizeOfTag); } static @@ -4456,15 +4719,25 @@ cmsBool WriteSegmentedCurve(cmsIOHANDLER* io, cmsToneCurve* g) } else { int Type; +#ifdef CMS_USE_ICCMAX_SPECTRAL + cmsUInt32Number ParamsByType[] = { 4, 5, 5, 5, 5, 6, 7, 6 }; +#else cmsUInt32Number ParamsByType[] = { 4, 5, 5 }; +#endif // This is a formula-based if (!_cmsWriteUInt32Number(io, (cmsUInt32Number) cmsSigFormulaCurveSeg)) goto Error; if (!_cmsWriteUInt32Number(io, 0)) goto Error; +#ifdef CMS_USE_ICCMAX_SPECTRAL + // ICC.2 Table 111 function types 0..7 + Type = ActualSeg ->Type - 6; + if (Type > 7 || Type < 0) goto Error; +#else // We only allow 1, 2 and 3 as types Type = ActualSeg ->Type - 6; if (Type > 2 || Type < 0) goto Error; +#endif if (!_cmsWriteUInt16Number(io, (cmsUInt16Number) Type)) goto Error; if (!_cmsWriteUInt16Number(io, 0)) goto Error; @@ -4485,6 +4758,66 @@ cmsBool WriteSegmentedCurve(cmsIOHANDLER* io, cmsToneCurve* g) } +#ifdef CMS_USE_ICCMAX_SPECTRAL +// TRUE when the curve still has the exact three segment shape ReadSingleSampledCurve +// builds, which is the only shape the 'sngf' encoding can hold: the middle segment +// holds the samples over [F, L] and the outer two carry the extension. CurveType +// survives cmsDupToneCurve, so a curve can carry the flag and yet have been reshaped +// since; WriteMPECurve consults this before committing to the 'sngf' form. +static +cmsBool IsSingleSampledShape(const cmsToneCurve* g) +{ + if (g ->nSegments != 3) return FALSE; + + if (g ->Segments[1].Type != 0 || g ->Segments[1].SampledPoints == NULL) return FALSE; + if (g ->Segments[1].nGridPoints < 2) return FALSE; + + // The outer two must still be the formula type 6 extensions ReadSingleSampledCurve + // built. WriteSingleSampledCurve reads Segments[0].Params[1] as the extension slope + // to decide the extensionType flag, and Params[1] means something else entirely for + // any other segment type -- so without this the flag would be derived from an + // unrelated parameter of an unrelated formula. + if (g ->Segments[0].Type != 6 || g ->Segments[2].Type != 6) return FALSE; + + return TRUE; +} + +// Write a curve that came in as an iccMAX singleSampledCurve back in that form +// (ICC.2:2023 Table 108). Always emitted as float32Number, which is lossless whatever +// it was read as. +static +cmsBool WriteSingleSampledCurve(cmsIOHANDLER* io, cmsToneCurve* g) +{ + cmsUInt32Number i; + cmsCurveSegment* Sampled; + cmsUInt16Number ExtensionType; + + if (!IsSingleSampledShape(g)) return FALSE; + + Sampled = &g ->Segments[1]; + + // A zero slope on the lower extension means it clips, anything else extrapolates. + // ReadSingleSampledCurve only ever writes those two shapes. + ExtensionType = (g ->Segments[0].Params[1] == 0.0) ? 0U : 1U; + + if (!_cmsWriteUInt32Number(io, (cmsUInt32Number) cmsSigSingleSampledCurve)) return FALSE; + if (!_cmsWriteUInt32Number(io, 0)) return FALSE; + if (!_cmsWriteUInt32Number(io, Sampled ->nGridPoints)) return FALSE; + if (!_cmsWriteFloat32Number(io, Sampled ->x0)) return FALSE; + if (!_cmsWriteFloat32Number(io, Sampled ->x1)) return FALSE; + if (!_cmsWriteUInt16Number(io, ExtensionType)) return FALSE; + if (!_cmsWriteUInt16Number(io, 0)) return FALSE; // valueEncodingType float32Number + + for (i = 0; i < Sampled ->nGridPoints; i++) { + + if (!_cmsWriteFloat32Number(io, Sampled ->SampledPoints[i])) return FALSE; + } + + return TRUE; +} +#endif // CMS_USE_ICCMAX_SPECTRAL + + static cmsBool WriteMPECurve(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, @@ -4494,6 +4827,16 @@ cmsBool WriteMPECurve(struct _cms_typehandler_struct* self, { _cmsStageToneCurvesData* Curves = (_cmsStageToneCurvesData*) Cargo; +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Preserve the encoding the curve arrived in, but only while the curve still has a + // shape the 'sngf' form can express. Anything that reshaped the segments while + // keeping CurveType falls back to a segmented curve, which encodes the same + // function, rather than failing the whole profile write. + if (Curves ->TheCurves[n] ->CurveType == cmsSigSingleSampledCurve && + IsSingleSampledShape(Curves ->TheCurves[n])) + return WriteSingleSampledCurve(io, Curves ->TheCurves[n]); +#endif + return WriteSegmentedCurve(io, Curves ->TheCurves[n]); cmsUNUSED_PARAMETER(SizeOfTag); @@ -4543,10 +4886,20 @@ void *Type_MPEmatrix_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io if (!_cmsReadUInt16Number(io, &OutputChans)) return NULL; - // Input and output chans may be ANY (up to 0xffff), +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Input and output chans may be ANY (up to 0xffff), but we choose to limit to + // MAX_STAGE_CHANNELS. That is higher than cmsMAXCHANNELS because an iccMAX + // spectral pipeline typically ends in a matrix that expands a handful of basis + // coefficients out to the full spectrum, e.g. 6 to 36. Both the storage and + // EvaluateMatrix are fully dynamic, so only the pipeline buffers set the ceiling. + if (InputChans >= MAX_STAGE_CHANNELS) return NULL; + if (OutputChans >= MAX_STAGE_CHANNELS) return NULL; +#else + // Input and output chans may be ANY (up to 0xffff), // but we choose to limit to 16 channels for now if (InputChans >= cmsMAXCHANNELS) return NULL; if (OutputChans >= cmsMAXCHANNELS) return NULL; +#endif nElems = (cmsUInt32Number) InputChans * OutputChans; @@ -4645,8 +4998,24 @@ void *Type_MPEclut_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, if (!_cmsReadUInt16Number(io, &InputChans)) return NULL; if (!_cmsReadUInt16Number(io, &OutputChans)) return NULL; + // Input: ICC.2 Table 113 requires 1 <= P <= 16, the same bound extendedCLUTElement + // carries in Table 117. Little-CMS stops one short of it, because MAX_INPUT_DIMENSIONS + // is 15 and GridPoints[] below is sized by it. That is a pre-existing structural limit + // of this reader, identical in the xclt reader, and not specific to iccMAX. if (InputChans == 0 || InputChans >= cmsMAXCHANNELS) goto Error; + + // Output: Table 113 places no bound on Q at all -- the spec pins an element's output + // count only where it carries meaning, as in emissionCLUTElement and + // reflectanceCLUTElement, which both require Q = 3. Multi process elements are not + // constrained by the 16 channel ceiling that applies to a profile's colour spaces, + // in v4 or in v5, so a spectral transform may legitimately be built from a plain + // clutElement rather than the extended xclt form. Accept the same ceiling the xclt + // reader uses. The default build keeps master's bound exactly. +#ifdef CMS_USE_ICCMAX_SPECTRAL + if (OutputChans == 0 || OutputChans >= MAX_STAGE_CHANNELS) goto Error; +#else if (OutputChans == 0 || OutputChans >= cmsMAXCHANNELS) goto Error; +#endif if (io ->Read(io, Dimensions8, sizeof(cmsUInt8Number), 16) != 16) goto Error; @@ -4719,6 +5088,220 @@ cmsBool Type_MPEclut_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* +#ifdef CMS_USE_ICCMAX_SPECTRAL +// ******************************************************************************** +// Type cmsSigExtCLutElemType ('xclt') +// +// The iccMAX extendedCLUTElement (ICC.2:2023 11.2.7, Table 117). Same grid layout +// and interpolation as the ICC.1 clutElement, but the samples may be encoded as +// float32Number, float16Number, uInt16Number or uInt8Number, selected by a +// valueEncodingType field (ICC.2:2023 4.2.10, Table 8). +// +// Table 117 is: +// 0..3 'xclt' signature ] consumed by ReadMPEElem/Type_MPE_Write, +// 4..6 reserved, shall be 0 ] which read/write the signature plus 4 bytes +// 7 interpolation hint ] so this handler starts at byte 8. The hint +// is advisory and is not retained. +// 8..9 input channels (P) uInt16Number +// 10..11 output channels (Q) uInt16Number +// 12..13 CLUT encoding type uInt16Number as valueEncodingType +// 14..15 reserved, shall be 0 +// 16..31 grid points per channel uInt8Number[16] +// 32.. CLUT data per encoding type +// +// Table 117 describes bytes 12 to 15 as a single uInt32Number, but 4.2.10 permits a +// valueEncodingType to be encoded as either a uInt16Number or a uInt32Number, and +// real iccMAX profiles put a uInt16Number in bytes 12 and 13 followed by two +// reserved bytes. So does the reference implementation, CIccMpeExtCLUT::Read and +// ::Write, which does Read16(storageType) then Read16(reserved). That is the layout +// written here. Both spellings are accepted on read, which is unambiguous: the +// uInt16 form always leaves bytes 14 and 15 zero, and the two only coincide for +// float32Number, where either way all four bytes are zero. +// +// Table 117 permits P up to 16, but MAX_INPUT_DIMENSIONS is 15, so 16 is rejected +// rather than silently truncated. +// ******************************************************************************** + +static +void *Type_MPEextclut_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag) +{ + cmsStage* mpe = NULL; + cmsUInt16Number InputChans, OutputChans; + cmsUInt8Number Dimensions8[16]; + cmsUInt32Number i, nMaxGrids, GridPoints[MAX_INPUT_DIMENSIONS]; + cmsUInt16Number EncodingType, Reserved2; + cmsUInt32Number nEntries, BytesPerSample; + _cmsStageCLutData* clut; + + // Bytes of the element consumed before the CLUT data starts: the 'xclt' signature + // and the reserved word read by ReadMPEElem, plus Table 117's own 24 byte header + // (channels, encoding type, reserved and the 16 grid point bytes) read below. +#define EXTCLUT_HEADER_BYTES 32 + + *nItems = 0; + + if (!_cmsReadUInt16Number(io, &InputChans)) goto Error; + if (!_cmsReadUInt16Number(io, &OutputChans)) goto Error; + + if (InputChans == 0 || InputChans >= cmsMAXCHANNELS) goto Error; + if (OutputChans == 0 || OutputChans >= MAX_STAGE_CHANNELS) goto Error; + + if (!_cmsReadUInt16Number(io, &EncodingType)) goto Error; + if (!_cmsReadUInt16Number(io, &Reserved2)) goto Error; + + // Tolerate the field written as a uInt32Number. Reserved bytes shall be zero, so + // a non zero low half with a zero high half can only be the uInt32 spelling. + if (EncodingType == 0 && Reserved2 != 0) EncodingType = Reserved2; + + if (io ->Read(io, Dimensions8, sizeof(cmsUInt8Number), 16) != 16) + goto Error; + + // Copy MAX_INPUT_DIMENSIONS at most. Expand to cmsUInt32Number + nMaxGrids = InputChans > MAX_INPUT_DIMENSIONS ? (cmsUInt32Number) MAX_INPUT_DIMENSIONS : InputChans; + + for (i = 0; i < nMaxGrids; i++) { + if (Dimensions8[i] == 1) goto Error; // Impossible value, 0 for no CLUT and then 2 at least + GridPoints[i] = (cmsUInt32Number)Dimensions8[i]; + } + + // How many bytes each sample takes, per ICC.2:2023 Table 8 + switch (EncodingType) { + + case 0: BytesPerSample = 4; break; // float32Number + case 1: BytesPerSample = 2; break; // float16Number + case 2: BytesPerSample = 2; break; // uInt16Number + case 3: BytesPerSample = 1; break; // uInt8Number + + default: + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, + "Unknown extendedCLUT value encoding type '%d'", EncodingType); + goto Error; + } + + // Work out how many samples the declared grid implies, and refuse the element if + // the data it claims to hold does not fit in the bytes the position table gave it. + // Without this a small element can declare a huge grid, and the parser would only + // notice once the reads below ran off the end of the profile. + nEntries = OutputChans; + + for (i = 0; i < nMaxGrids; i++) { + + if (GridPoints[i] == 0 || nEntries > UINT_MAX / GridPoints[i]) goto Error; + nEntries *= GridPoints[i]; + } + + if (SizeOfTag < EXTCLUT_HEADER_BYTES) goto Error; + if (nEntries > (SizeOfTag - EXTCLUT_HEADER_BYTES) / BytesPerSample) goto Error; + + // Allocate the true CLUT + mpe = cmsStageAllocCLutFloatGranular(self ->ContextID, GridPoints, InputChans, OutputChans, NULL); + if (mpe == NULL) goto Error; + + // Remember that this element came in as an extendedCLUT, so it is written back + // as one. Implements stays at cmsSigCLutElemType: it *is* a CLUT, it is merely + // encoded differently. + mpe ->Type = (cmsStageSignature) cmsSigExtCLutElemType; + + clut = (_cmsStageCLutData*) mpe ->Data; + + // Read the data in whichever encoding the element declares + switch (EncodingType) { + + case 0: // float32Number + for (i = 0; i < clut ->nEntries; i++) { + + if (!_cmsReadFloat32Number(io, &clut->Tab.TFloat[i])) goto Error; + } + break; + + case 1: // float16Number + for (i = 0; i < clut ->nEntries; i++) { + + if (!_cmsReadFloat16Number(io, &clut->Tab.TFloat[i])) goto Error; + } + break; + + case 2: // uInt16Number, encoding 0.0 to 1.0 + for (i = 0; i < clut ->nEntries; i++) { + + cmsUInt16Number v; + + if (!_cmsReadUInt16Number(io, &v)) goto Error; + clut->Tab.TFloat[i] = (cmsFloat32Number) v / 65535.0f; + } + break; + + case 3: // uInt8Number, encoding 0.0 to 1.0 + for (i = 0; i < clut ->nEntries; i++) { + + cmsUInt8Number v; + + if (!_cmsReadUInt8Number(io, &v)) goto Error; + clut->Tab.TFloat[i] = (cmsFloat32Number) v / 255.0f; + } + break; + + default: + cmsSignalError(self ->ContextID, cmsERROR_UNKNOWN_EXTENSION, + "Unknown extendedCLUT value encoding type '%d'", EncodingType); + goto Error; + } + + *nItems = 1; + return mpe; + +Error: + *nItems = 0; + if (mpe != NULL) cmsStageFree(mpe); + return NULL; +} + +#undef EXTCLUT_HEADER_BYTES + +// Write an extended CLUT. Always emitted as float32Number, which is lossless for +// whatever encoding it was read from. +static +cmsBool Type_MPEextclut_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, void* Ptr, cmsUInt32Number nItems) +{ + cmsUInt8Number Dimensions8[16]; // 16 because the spec says 16 and not max number of channels + cmsUInt32Number i; + cmsStage* mpe = (cmsStage*) Ptr; + _cmsStageCLutData* clut = (_cmsStageCLutData*) mpe ->Data; + + // Check for maximum number of channels supported by lcms + if (mpe -> InputChannels > MAX_INPUT_DIMENSIONS) return FALSE; + + // Only floats are supported in MPE + if (clut ->HasFloatValues == FALSE) return FALSE; + + if (!_cmsWriteUInt16Number(io, (cmsUInt16Number) mpe ->InputChannels)) return FALSE; + if (!_cmsWriteUInt16Number(io, (cmsUInt16Number) mpe ->OutputChannels)) return FALSE; + + // valueEncodingType 0, float32Number, as a uInt16Number plus 2 reserved bytes + if (!_cmsWriteUInt16Number(io, 0)) return FALSE; + if (!_cmsWriteUInt16Number(io, 0)) return FALSE; + + memset(Dimensions8, 0, sizeof(Dimensions8)); + + for (i=0; i < mpe ->InputChannels; i++) + Dimensions8[i] = (cmsUInt8Number) clut ->Params ->nSamples[i]; + + if (!io ->Write(io, 16, Dimensions8)) return FALSE; + + for (i=0; i < clut ->nEntries; i++) { + + if (!_cmsWriteFloat32Number(io, clut ->Tab.TFloat[i])) return FALSE; + } + + return TRUE; + + cmsUNUSED_PARAMETER(nItems); + cmsUNUSED_PARAMETER(self); +} +#endif // CMS_USE_ICCMAX_SPECTRAL + + + // This is the list of built-in MPE types static _cmsTagTypeLinkedList SupportedMPEtypes[] = { @@ -4727,7 +5310,13 @@ static _cmsTagTypeLinkedList SupportedMPEtypes[] = { {TYPE_MPE_HANDLER((cmsTagTypeSignature) cmsSigCurveSetElemType, MPEcurve), &SupportedMPEtypes[3] }, {TYPE_MPE_HANDLER((cmsTagTypeSignature) cmsSigMatrixElemType, MPEmatrix), &SupportedMPEtypes[4] }, -{TYPE_MPE_HANDLER((cmsTagTypeSignature) cmsSigCLutElemType, MPEclut), NULL }, +{TYPE_MPE_HANDLER((cmsTagTypeSignature) cmsSigCLutElemType, MPEclut), +#ifdef CMS_USE_ICCMAX_SPECTRAL + &SupportedMPEtypes[5] }, +{TYPE_MPE_HANDLER((cmsTagTypeSignature) cmsSigExtCLutElemType, MPEextclut), NULL }, +#else + NULL }, +#endif }; _cmsTagTypePluginChunkType _cmsMPETypePluginChunk = { NULL }; @@ -4796,8 +5385,15 @@ void *Type_MPE_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsU if (!_cmsReadUInt16Number(io, &InputChans)) return NULL; if (!_cmsReadUInt16Number(io, &OutputChans)) return NULL; +#ifdef CMS_USE_ICCMAX_SPECTRAL + // MAX_STAGE_CHANNELS, not cmsMAXCHANNELS: an iccMAX DToBx pipeline may output + // a spectral vector of 37 or more channels + if (InputChans == 0 || InputChans >= MAX_STAGE_CHANNELS) return NULL; + if (OutputChans == 0 || OutputChans >= MAX_STAGE_CHANNELS) return NULL; +#else if (InputChans == 0 || InputChans >= cmsMAXCHANNELS) return NULL; if (OutputChans == 0 || OutputChans >= cmsMAXCHANNELS) return NULL; +#endif // Allocates an empty LUT NewLUT = cmsPipelineAlloc(self ->ContextID, InputChans, OutputChans); @@ -5923,6 +6519,436 @@ void* Type_MHC2_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cms +#ifdef CMS_USE_ICCMAX_SPECTRAL +// ******************************************************************************** +// Type cmsSigEmbeddedProfileType ('ICCp') +// +// Holds an embedded ICC.2 (iccMAX) profile, per the ICC technical note "Embedding +// an ICC.2 (iccMAX) profile in an ICC.1 profile", Table 1: +// +// 0..3 'ICCp' type signature ] this is exactly the standard tag base, which +// 4..7 reserved, shall be 0 ] _cmsReadTypeBase already consumed +// 8.. the ICC.2 profile, in its entirety +// +// so SizeOfTag is the length of the embedded profile and no offset maths is needed. +// Returned as a cmsICCData whose data can be handed straight to +// cmsOpenProfileFromMem; len must live in the object because DupPtr is called with +// the tag descriptor's ElemCount, not a byte count. The bytes are passed through +// verbatim on write: nothing here authors or rewrites ICC.2 content. +// ******************************************************************************** + +static +void *Type_EmbeddedProfile_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag) +{ + cmsICCData* Embedded; + + *nItems = 0; + + // A profile is at least a 128 byte header plus a 4 byte tag count + if (SizeOfTag < 132) return NULL; + if (SizeOfTag > INT_MAX) return NULL; + + Embedded = (cmsICCData*) _cmsMalloc(self ->ContextID, sizeof(cmsICCData) + SizeOfTag - 1); + if (Embedded == NULL) return NULL; + + Embedded ->len = SizeOfTag; + Embedded ->flag = 0; + + if (io ->Read(io, Embedded ->data, sizeof(cmsUInt8Number), SizeOfTag) != SizeOfTag) { + + _cmsFree(self ->ContextID, Embedded); + return NULL; + } + + *nItems = 1; + return (void*) Embedded; +} + +static +cmsBool Type_EmbeddedProfile_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, void* Ptr, cmsUInt32Number nItems) +{ + cmsICCData* Embedded = (cmsICCData*) Ptr; + + return io ->Write(io, Embedded ->len, Embedded ->data); + + cmsUNUSED_PARAMETER(nItems); + cmsUNUSED_PARAMETER(self); +} + +static +void* Type_EmbeddedProfile_Dup(struct _cms_typehandler_struct* self, const void *Ptr, cmsUInt32Number n) +{ + cmsICCData* Embedded = (cmsICCData*) Ptr; + + return _cmsDupMem(self ->ContextID, Ptr, sizeof(cmsICCData) + Embedded ->len - 1); + + cmsUNUSED_PARAMETER(n); +} + +static +void Type_EmbeddedProfile_Free(struct _cms_typehandler_struct* self, void* Ptr) +{ + _cmsFree(self ->ContextID, Ptr); +} +#endif // CMS_USE_ICCMAX_SPECTRAL + + +#ifdef CMS_USE_ICCMAX_SPECTRAL + +// ******************************************************************************** +// Type float16ArrayType and float32ArrayType (ICC.2:2023 10.2.9 and 10.2.10) +// ******************************************************************************** +// +// Both are the type signature, four reserved bytes, then a bare vector of values; +// ICC.2 derives the count from the tag size. The framework has already consumed +// the signature and the reserved bytes by the time a reader is called, so +// SizeOfTag counts the values only. Both encodings are presented to callers as one +// cmsFloatArray, tagged with the encoding it came from, so that a profile authored +// in half precision does not silently get rewritten as single precision. + +static +void* ReadFloatArray(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, + cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag, + cmsUInt32Number BytesPerValue) +{ + cmsFloatArray* v; + cmsUInt32Number i, n; + + *nItems = 0; + + n = SizeOfTag / BytesPerValue; + if (n == 0) return NULL; + + v = cmsAllocFloatArray(self ->ContextID, n); + if (v == NULL) return NULL; + + for (i = 0; i < n; i++) { + + if (BytesPerValue == 2) { + + if (!_cmsReadFloat16Number(io, &v ->Values[i])) goto Error; + } + else { + + if (!_cmsReadFloat32Number(io, &v ->Values[i])) goto Error; + } + } + + *nItems = 1; + return (void*) v; + +Error: + cmsFreeFloatArray(v); + return NULL; +} + +static +cmsBool WriteFloatArray(cmsIOHANDLER* io, void* Ptr, cmsUInt32Number BytesPerValue) +{ + cmsFloatArray* v = (cmsFloatArray*) Ptr; + cmsUInt32Number i; + + if (v == NULL || v ->Values == NULL) return FALSE; + + // The length comes from the object. nItems is TagDescriptor->ElemCount, which + // for swpt is the constant 1 and has nothing to do with the value count. + for (i = 0; i < v ->nValues; i++) { + + if (BytesPerValue == 2) { + + if (!_cmsWriteFloat16Number(io, v ->Values[i])) return FALSE; + } + else { + + if (!_cmsWriteFloat32Number(io, v ->Values[i])) return FALSE; + } + } + + return TRUE; +} + +static +void* Type_FloatArray_Dup(struct _cms_typehandler_struct* self, const void* Ptr, cmsUInt32Number n) +{ + const cmsFloatArray* v = (const cmsFloatArray*) Ptr; + cmsFloatArray* NewArray; + + if (v == NULL) return NULL; + + // Again, the length is v->nValues and not n + NewArray = cmsAllocFloatArray(self ->ContextID, v ->nValues); + if (NewArray == NULL) return NULL; + + memcpy(NewArray ->Values, v ->Values, v ->nValues * sizeof(cmsFloat32Number)); + + return (void*) NewArray; + + cmsUNUSED_PARAMETER(n); +} + +static +void Type_FloatArray_Free(struct _cms_typehandler_struct* self, void* Ptr) +{ + cmsFreeFloatArray((cmsFloatArray*) Ptr); + + cmsUNUSED_PARAMETER(self); +} + +#define Type_Float16Array_Dup Type_FloatArray_Dup +#define Type_Float16Array_Free Type_FloatArray_Free +#define Type_Float32Array_Dup Type_FloatArray_Dup +#define Type_Float32Array_Free Type_FloatArray_Free + +static +void* Type_Float16Array_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag) +{ + return ReadFloatArray(self, io, nItems, SizeOfTag, 2); +} + +static +cmsBool Type_Float16Array_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, void* Ptr, cmsUInt32Number nItems) +{ + return WriteFloatArray(io, Ptr, 2); + + cmsUNUSED_PARAMETER(self); + cmsUNUSED_PARAMETER(nItems); +} + +static +void* Type_Float32Array_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag) +{ + return ReadFloatArray(self, io, nItems, SizeOfTag, 4); +} + +static +cmsBool Type_Float32Array_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, void* Ptr, cmsUInt32Number nItems) +{ + return WriteFloatArray(io, Ptr, 4); + + cmsUNUSED_PARAMETER(self); + cmsUNUSED_PARAMETER(nItems); +} + +// swpt may be encoded as fl16, fl32 or ui16 (ICC.2 9.2.112). Only the two float +// encodings are registered here -- see cmsWriteSpectralWhitePoint for ui16, which +// cannot be a registered handler without imposing swpt's 1/65535 scaling on every +// future user of the generic ui16 signature. With no DecideType hook, both +// cmsWriteTag and SaveTags fall back to SupportedTypes[0] (fl32), which is +// lossless from fl16 -- Little-CMS does not preserve tag encodings elsewhere +// either, so a tag read as fl16 is always written back as fl32. + +// ******************************************************************************** +// Type spectralViewingConditionsType (ICC.2:2023 Table 69) +// ******************************************************************************** + +static +void* Type_SpectralViewingConditions_Read(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, + cmsUInt32Number* nItems, cmsUInt32Number SizeOfTag) +{ + cmsSpectralViewingConditions* sv = NULL; + cmsFloat32Number* Observer = NULL; + cmsUInt32Number ObserverType, IlluminantType; + cmsFloat32Number ObserverStart, ObserverEnd, IlluminantStart, IlluminantEnd, CCT; + cmsUInt16Number N, M, Reserved; + cmsUInt32Number i, n3; + cmsFloat32Number fx, fy, fz; + + *nItems = 0; + + // Observer: type, spectral range, two reserved bytes. Twelve bytes so far. + if (SizeOfTag < 12) return NULL; + + if (!_cmsReadUInt32Number(io, &ObserverType)) return NULL; + if (!_cmsReadFloat16Number(io, &ObserverStart)) return NULL; + if (!_cmsReadFloat16Number(io, &ObserverEnd)) return NULL; + if (!_cmsReadUInt16Number(io, &N)) return NULL; + if (!_cmsReadUInt16Number(io, &Reserved)) return NULL; + + // ICC.2 10.2.22 permits N == 0 when the observer type is non-zero and one of + // the Table 70 standard observers: the range then defaults to 380-780 nm in + // 81 steps of the standard CMF data, and the matrix is simply absent from the + // wire. This is a known, deliberate limitation, not an oversight: Little-CMS + // does not carry the standard CIE 1931 / 1964 CMF tables needed to populate + // Observer in that case, so rather than half-read the tag (leaving Observer + // NULL while claiming success) this rejects it outright. A future change + // that ships the standard tables could implement the fallback here. + if (N == 0) return NULL; + + // The observer matrix plus the illuminant's own 16 byte header must fit. N is a + // uInt16 so 12*N cannot overflow 32 bits. + n3 = 3 * (cmsUInt32Number) N; + + if (SizeOfTag < 12 + 4 * n3 + 16) return NULL; + + Observer = (cmsFloat32Number*) _cmsCalloc(self ->ContextID, n3, sizeof(cmsFloat32Number)); + if (Observer == NULL) return NULL; + + for (i = 0; i < n3; i++) + if (!_cmsReadFloat32Number(io, &Observer[i])) goto Error; + + // Illuminant: type, CCT, spectral range, two reserved bytes + if (!_cmsReadUInt32Number(io, &IlluminantType)) goto Error; + if (!_cmsReadFloat32Number(io, &CCT)) goto Error; + if (!_cmsReadFloat16Number(io, &IlluminantStart)) goto Error; + if (!_cmsReadFloat16Number(io, &IlluminantEnd)) goto Error; + if (!_cmsReadUInt16Number(io, &M)) goto Error; + if (!_cmsReadUInt16Number(io, &Reserved)) goto Error; + + if (M == 0) goto Error; + + // The illuminant vector plus the two trailing XYZ triples (24 bytes total, + // float32 -- see the comment further down) must fit + if (SizeOfTag < 12 + 4 * n3 + 16 + 4 * (cmsUInt32Number) M + 24) goto Error; + + sv = cmsAllocSpectralViewingConditions(self ->ContextID, N, M); + if (sv == NULL) goto Error; + + memcpy(sv ->Observer, Observer, n3 * sizeof(cmsFloat32Number)); + _cmsFree(self ->ContextID, Observer); + Observer = NULL; + + sv ->ObserverType = ObserverType; + sv ->ObserverStart = ObserverStart; + sv ->ObserverEnd = ObserverEnd; + sv ->IlluminantType = IlluminantType; + sv ->CCT = CCT; + sv ->IlluminantStart = IlluminantStart; + sv ->IlluminantEnd = IlluminantEnd; + + for (i = 0; i < (cmsUInt32Number) M; i++) + if (!_cmsReadFloat32Number(io, &sv ->Illuminant[i])) goto Error; + + // ICC.2:2023 Table 69 lists both trailing triples as XYZNumber (s15Fixed16), + // but that is a spec-table error: the reference implementation + // (IccProfLib icProfileHeader.h icFloatXYZNumber, three icFloat32Number) and + // CIccTagSpectralViewingConditions read/write these with float32, and every + // real profile -- including this branch's own committed fixture -- carries + // them that way. lcms follows the reference rather than the spec table, so + // an s15Fixed16 read here would return physically nonsensical values (e.g. + // the fixture's D50-at-160cd/m2 illuminant would come back as ~17184 cd/m2, + // a near equal-energy white at an absurd luminance). + if (!_cmsReadFloat32Number(io, &fx)) goto Error; + if (!_cmsReadFloat32Number(io, &fy)) goto Error; + if (!_cmsReadFloat32Number(io, &fz)) goto Error; + sv ->IlluminantXYZ.X = fx; + sv ->IlluminantXYZ.Y = fy; + sv ->IlluminantXYZ.Z = fz; + + if (!_cmsReadFloat32Number(io, &fx)) goto Error; + if (!_cmsReadFloat32Number(io, &fy)) goto Error; + if (!_cmsReadFloat32Number(io, &fz)) goto Error; + sv ->SurroundXYZ.X = fx; + sv ->SurroundXYZ.Y = fy; + sv ->SurroundXYZ.Z = fz; + + *nItems = 1; + return (void*) sv; + +Error: + if (Observer != NULL) _cmsFree(self ->ContextID, Observer); + if (sv != NULL) cmsFreeSpectralViewingConditions(sv); + return NULL; +} + +static +cmsBool Type_SpectralViewingConditions_Write(struct _cms_typehandler_struct* self, cmsIOHANDLER* io, + void* Ptr, cmsUInt32Number nItems) +{ + cmsSpectralViewingConditions* sv = (cmsSpectralViewingConditions*) Ptr; + cmsUInt32Number i, n3, m; + cmsFloat32Number fx, fy, fz; + + if (sv == NULL || sv ->Observer == NULL || sv ->Illuminant == NULL) return FALSE; + if (sv ->ObserverSteps == 0 || sv ->IlluminantSteps == 0) return FALSE; + + n3 = 3 * (cmsUInt32Number) sv ->ObserverSteps; + m = (cmsUInt32Number) sv ->IlluminantSteps; + + if (!_cmsWriteUInt32Number(io, sv ->ObserverType)) return FALSE; + if (!_cmsWriteFloat16Number(io, sv ->ObserverStart)) return FALSE; + if (!_cmsWriteFloat16Number(io, sv ->ObserverEnd)) return FALSE; + if (!_cmsWriteUInt16Number(io, sv ->ObserverSteps)) return FALSE; + if (!_cmsWriteUInt16Number(io, 0)) return FALSE; // reserved + + for (i = 0; i < n3; i++) + if (!_cmsWriteFloat32Number(io, sv ->Observer[i])) return FALSE; + + if (!_cmsWriteUInt32Number(io, sv ->IlluminantType)) return FALSE; + if (!_cmsWriteFloat32Number(io, sv ->CCT)) return FALSE; + if (!_cmsWriteFloat16Number(io, sv ->IlluminantStart)) return FALSE; + if (!_cmsWriteFloat16Number(io, sv ->IlluminantEnd)) return FALSE; + if (!_cmsWriteUInt16Number(io, sv ->IlluminantSteps)) return FALSE; + if (!_cmsWriteUInt16Number(io, 0)) return FALSE; // reserved + + for (i = 0; i < m; i++) + if (!_cmsWriteFloat32Number(io, sv ->Illuminant[i])) return FALSE; + + // See the matching comment in Type_SpectralViewingConditions_Read: ICC.2 + // Table 69 says XYZNumber for both trailing triples, but that is a spec-table + // error -- the reference implementation and every real profile use float32, + // so lcms writes float32 here rather than s15Fixed16. + fx = (cmsFloat32Number) sv ->IlluminantXYZ.X; + fy = (cmsFloat32Number) sv ->IlluminantXYZ.Y; + fz = (cmsFloat32Number) sv ->IlluminantXYZ.Z; + if (!_cmsWriteFloat32Number(io, fx)) return FALSE; + if (!_cmsWriteFloat32Number(io, fy)) return FALSE; + if (!_cmsWriteFloat32Number(io, fz)) return FALSE; + + fx = (cmsFloat32Number) sv ->SurroundXYZ.X; + fy = (cmsFloat32Number) sv ->SurroundXYZ.Y; + fz = (cmsFloat32Number) sv ->SurroundXYZ.Z; + if (!_cmsWriteFloat32Number(io, fx)) return FALSE; + if (!_cmsWriteFloat32Number(io, fy)) return FALSE; + if (!_cmsWriteFloat32Number(io, fz)) return FALSE; + + return TRUE; + + cmsUNUSED_PARAMETER(self); + cmsUNUSED_PARAMETER(nItems); +} + +static +void* Type_SpectralViewingConditions_Dup(struct _cms_typehandler_struct* self, const void* Ptr, cmsUInt32Number n) +{ + const cmsSpectralViewingConditions* sv = (const cmsSpectralViewingConditions*) Ptr; + cmsSpectralViewingConditions* New; + + if (sv == NULL) return NULL; + + // Lengths come from the object, not from n, which is TagDescriptor->ElemCount + New = cmsAllocSpectralViewingConditions(self ->ContextID, sv ->ObserverSteps, sv ->IlluminantSteps); + if (New == NULL) return NULL; + + memcpy(New ->Observer, sv ->Observer, + 3 * (cmsUInt32Number) sv ->ObserverSteps * sizeof(cmsFloat32Number)); + memcpy(New ->Illuminant, sv ->Illuminant, + (cmsUInt32Number) sv ->IlluminantSteps * sizeof(cmsFloat32Number)); + + New ->ObserverType = sv ->ObserverType; + New ->ObserverStart = sv ->ObserverStart; + New ->ObserverEnd = sv ->ObserverEnd; + New ->IlluminantType = sv ->IlluminantType; + New ->CCT = sv ->CCT; + New ->IlluminantStart = sv ->IlluminantStart; + New ->IlluminantEnd = sv ->IlluminantEnd; + New ->IlluminantXYZ = sv ->IlluminantXYZ; + New ->SurroundXYZ = sv ->SurroundXYZ; + + return (void*) New; + + cmsUNUSED_PARAMETER(n); +} + +static +void Type_SpectralViewingConditions_Free(struct _cms_typehandler_struct* self, void* Ptr) +{ + cmsFreeSpectralViewingConditions((cmsSpectralViewingConditions*) Ptr); + + cmsUNUSED_PARAMETER(self); +} + +#endif // CMS_USE_ICCMAX_SPECTRAL + // ******************************************************************************** // Type support main routines // ******************************************************************************** @@ -5966,7 +6992,16 @@ static const _cmsTagTypeLinkedList SupportedTagTypes[] = { {TYPE_HANDLER(cmsSigMHC2Type, MHC2), (_cmsTagTypeLinkedList*) &SupportedTagTypes[33] }, {TYPE_HANDLER(cmsSigUInt8ArrayType, UInt8), (_cmsTagTypeLinkedList*) &SupportedTagTypes[34] }, {TYPE_HANDLER(cmsSigUInt32ArrayType, UInt32), (_cmsTagTypeLinkedList*) &SupportedTagTypes[35] }, -{TYPE_HANDLER(cmsSigUInt64ArrayType, UInt64), NULL } +{TYPE_HANDLER(cmsSigUInt64ArrayType, UInt64), +#ifdef CMS_USE_ICCMAX_SPECTRAL + (_cmsTagTypeLinkedList*) &SupportedTagTypes[36] }, +{TYPE_HANDLER(cmsSigEmbeddedProfileType, EmbeddedProfile), (_cmsTagTypeLinkedList*) &SupportedTagTypes[37] }, +{TYPE_HANDLER(cmsSigFloat16ArrayType, Float16Array), (_cmsTagTypeLinkedList*) &SupportedTagTypes[38] }, +{TYPE_HANDLER(cmsSigFloat32ArrayType, Float32Array), (_cmsTagTypeLinkedList*) &SupportedTagTypes[39] }, +{TYPE_HANDLER(cmsSigSpectralViewingConditionsType, SpectralViewingConditions), NULL } +#else + NULL } +#endif }; @@ -6163,7 +7198,24 @@ static _cmsTagLinkedList SupportedTags[] = { { cmsSigcicpTag, { 1, 1, { cmsSigcicpType}, NULL }, &SupportedTags[64]}, { cmsSigArgyllArtsTag, { 9, 1, { cmsSigS15Fixed16ArrayType}, NULL}, &SupportedTags[65]}, - { cmsSigMHC2Tag, { 1, 1, { cmsSigMHC2Type }, NULL}, NULL} + { cmsSigMHC2Tag, { 1, 1, { cmsSigMHC2Type }, NULL}, +#ifdef CMS_USE_ICCMAX_SPECTRAL + &SupportedTags[66]}, + + // An embedded ICC.2 profile. A CMM that does not support iccMAX sees this as a + // private tag and just uses the containing ICC.1 profile's transforms. + { cmsSigEmbeddedV5ProfileTag, { 1, 1, { cmsSigEmbeddedProfileType }, NULL}, &SupportedTags[67]}, + + // ICC.2 also permits uInt16ArrayType here; reach that through + // cmsReadSpectralWhitePoint / cmsWriteSpectralWhitePoint. + { cmsSigSpectralWhitePointTag, { 1, 2, { cmsSigFloat32ArrayType, cmsSigFloat16ArrayType }, + NULL }, &SupportedTags[68]}, + + { cmsSigSpectralViewingConditionsTag, + { 1, 1, { cmsSigSpectralViewingConditionsType }, NULL}, NULL} +#else + NULL} +#endif }; diff --git a/src/lcms2_internal.h b/src/lcms2_internal.h index 655c9aff7..a02e6fba8 100644 --- a/src/lcms2_internal.h +++ b/src/lcms2_internal.h @@ -830,6 +830,24 @@ typedef struct _cms_iccprofile_struct { cmsProfileID ProfileID; +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Guarded so a default build keeps master's layout of this struct byte for byte. + // These sit mid-struct, so without the guard every member below would shift and + // sizeof would grow -- and lcms2_internal.h is included by plugins/fast_float and + // plugins/threaded, which ship as separate libraries and would then disagree with + // liblcms2 about where TagCount and everything after it live. + // + // Spectral PCS, from the ICC.2 header fields that ICC.1 leaves reserved. + // Bytes 100-103, a spectral colour space signature (ICC.2:2023 7.2.21, Table 21) + cmsUInt32Number SpectralPCS; + + // Bytes 104-109, the spectral PCS wavelength range as a spectralRange + // (ICC.2:2023 7.2.22 and 4.2.8): start and end as float16, steps as uInt16 + cmsFloat32Number SpectralPCSStart; + cmsFloat32Number SpectralPCSEnd; + cmsUInt16Number SpectralPCSSteps; +#endif // CMS_USE_ICCMAX_SPECTRAL + // Dictionary cmsUInt32Number TagCount; cmsTagSignature TagNames[MAX_TABLE_TAG]; @@ -890,6 +908,18 @@ struct _cms_curve_struct { // 16 bit Table-based representation follows cmsUInt32Number nEntries; // Number of table elements cmsUInt16Number* Table16; // The table itself. + +#ifdef CMS_USE_ICCMAX_SPECTRAL + // Appended at the end so every member above keeps master's offset, and guarded so a + // default build keeps master's sizeof as well -- plugins/fast_float and + // plugins/threaded include this header and allocate no curves themselves, but a + // mismatched sizeof between a plugin and liblcms2 is a trap worth not setting. + // + // How the curve was encoded, so a curve read from an iccMAX singleSampledCurve is + // written back as one instead of being promoted to a segmentedCurve. Zero, the + // default from the zeroing allocator, means segmentedCurve. + cmsCurveSegSignature CurveType; +#endif // CMS_USE_ICCMAX_SPECTRAL }; @@ -1038,6 +1068,13 @@ CMSCHECKPOINT cmsFormatter CMSEXPORT _cmsGetFormatter(cmsContext ContextID, CMSCHECKPOINT cmsFloat32Number CMSEXPORT _cmsHalf2Float(cmsUInt16Number h); CMSCHECKPOINT cmsUInt16Number CMSEXPORT _cmsFloat2Half(cmsFloat32Number flt); +#ifdef CMS_USE_ICCMAX_SPECTRAL +// float16Number IO, as used by iccMAX spectralRange, extendedCLUTElement and +// singleSampledCurve. Conversion is done by the two functions above. +cmsBool CMSEXPORT _cmsReadFloat16Number(cmsIOHANDLER* io, cmsFloat32Number* n); +cmsBool CMSEXPORT _cmsWriteFloat16Number(cmsIOHANDLER* io, cmsFloat32Number n); +#endif + #endif // Transform logic ------------------------------------------------------------------------------------------------------ diff --git a/testbed/HybridPrinterCMYK_small.icc b/testbed/HybridPrinterCMYK_small.icc new file mode 100644 index 0000000000000000000000000000000000000000..85b5777b043ce289a4fbc21103bf17958024b21f GIT binary patch literal 66552 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
    Y_&@Rz>Xy=({81A$k%vUfTogfwx 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 Rd0)zZT;Tt7{eQ#e{{VYY8cF~F literal 0 HcmV?d00001 diff --git a/testbed/Makefile.am b/testbed/Makefile.am index c8bacf54b..45dce3bbd 100644 --- a/testbed/Makefile.am +++ b/testbed/Makefile.am @@ -20,7 +20,8 @@ testcms_SOURCES = testcms2.c testplugin.c zoo_icc.c testcms2.h EXTRA_DIST = test1.icc bad.icc toosmall.icc test2.icc \ test3.icc test4.icc \ - test5.icc ibm-t61.icc crayons.icc bad_mpe.icc + test5.icc ibm-t61.icc crayons.icc bad_mpe.icc \ + HybridPrinterCMYK_small.icc check: if [ $(top_srcdir) != $(top_builddir) ]; then \ diff --git a/testbed/Makefile.in b/testbed/Makefile.in index 51f3492ac..011dc8e40 100644 --- a/testbed/Makefile.in +++ b/testbed/Makefile.in @@ -347,7 +347,8 @@ testcms_LDFLAGS = -static @LDFLAGS@ testcms_SOURCES = testcms2.c testplugin.c zoo_icc.c testcms2.h EXTRA_DIST = test1.icc bad.icc toosmall.icc test2.icc \ test3.icc test4.icc \ - test5.icc ibm-t61.icc crayons.icc bad_mpe.icc + test5.icc ibm-t61.icc crayons.icc bad_mpe.icc \ + HybridPrinterCMYK_small.icc all: all-am diff --git a/testbed/meson.build b/testbed/meson.build index 361999fe2..6489c39b4 100644 --- a/testbed/meson.build +++ b/testbed/meson.build @@ -8,7 +8,7 @@ testcms_srcs = files( iccs = [ 'bad.icc', 'crayons.icc', 'test2.icc', 'test4.icc', 'bad_mpe.icc', 'ibm-t61.icc', 'test1.icc', 'test3.icc', 'test5.icc', - 'toosmall.icc' + 'toosmall.icc', 'HybridPrinterCMYK_small.icc' ] # fs=import('fs') - requires Meson >=0.53.0 foreach icc : iccs diff --git a/testbed/subsample_clut.py b/testbed/subsample_clut.py new file mode 100644 index 000000000..0bfa88a05 --- /dev/null +++ b/testbed/subsample_clut.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Reduce a hybrid ICC profile to a small test fixture by subsampling its CLUTs. + +Every CLUT is moved onto a coarser grid whose nodes coincide exactly with +existing nodes, so each retained value is an original value copied verbatim. +Also applies the de-branding metadata edits for the fixture. + +Pipeline (iccToXml and iccFromXml come from the DemoIccMAX tools): + + iccToXml HybridPrinterCMYK.icc full.xml + python subsample_clut.py full.xml small.xml + iccFromXml small.xml HybridPrinterCMYK_small.icc + +Design notes: + + * TableData is handled as whitespace-separated *tokens*, never parsed into + numbers. Bit-exact retention is therefore structural: no float formatting + or requantization can creep in. + * Target grid is chosen by input dimensionality: 4-input CLUTs go to 5 + points per axis, 3-input to 9. Stride is (G-1)/(Gn-1) and must divide + exactly, which is asserted rather than assumed. + * The metadata edits de-brand the fixture so it carries no vendor identity: + creator 'RICC', copyright "Copyright 2026 ICC.", preferred CMM zero, the + profile description set to the output filename, and the + UseWithEmbeddedDataOnly header flag cleared. + * The resulting fixture is structurally faithful -- every retained value is + an original node value -- but far too coarse to be colorimetrically + useful. Its ICC.1 and embedded ICC.2 renderings no longer track each + other. It exists to exercise readers, not to profile a device. +""" + +import re +import sys + +TARGET_BY_INPUTS = {4: 5, 3: 9} + +# Number of CLUTs the source profile is known to contain +EXPECTED_CLUTS = 8 + +CHANNELS_RE = re.compile(r'') + +CLUT_RE = re.compile( + r'\s*([^<]*)\s*(.*?)\s*', + re.DOTALL) + +EXTCLUT_RE = re.compile( + r'(]*>)\s*([^<]*)' + r'\s*(.*?)', + re.DOTALL) + +OUT_ATTR_RE = re.compile(r'OutputChannels="(\d+)"') + + +def fail(msg): + raise SystemExit("subsample_clut: " + msg) + + +def subsample(tokens, grid, out, new_g): + """Pick the tokens for a grid reduced from `grid` to `new_g` points per axis. + + Returns (list of per-point token lists, stride). + """ + dims = len(grid) + g = grid[0] + + if any(x != g for x in grid): + fail("non-uniform grid %s is not supported" % (grid,)) + if new_g < 2 or (g - 1) % (new_g - 1): + fail("stride from %d to %d points per axis is not an integer" % (g, new_g)) + stride = (g - 1) // (new_g - 1) + + expected = out + for x in grid: + expected *= x + if len(tokens) != expected: + fail("TableData holds %d values, expected %d (grid %s, %d out)" + % (len(tokens), expected, grid, out)) + + points = [] + + def walk(depth, flat): + # `flat` accumulates the ORIGINAL flat point index, first channel + # varying least rapidly (Horner over the original grid size) + if depth == dims: + points.append(tokens[flat * out: flat * out + out]) + return + for j in range(new_g): + walk(depth + 1, flat * g + j * stride) + + walk(0, 0) + return points, stride + + +def render(points, indent): + return "\n".join(indent + " ".join(p) for p in points) + + +def replace_exactly(text, old, new, count, what): + found = text.count(old) + if found != count: + fail("expected %d occurrence(s) of %s, found %d" % (count, what, found)) + return text.replace(old, new) + + +def edit_metadata(text): + text = replace_exactly(text, + "ONYX", + "", + 2, "PreferredCMMType") + + text = replace_exactly(text, + "ONYX", + "RICC", + 2, "ProfileCreator") + + text = replace_exactly(text, + 'UseWithEmbeddedDataOnly="true"', + 'UseWithEmbeddedDataOnly="false"', + 1, "UseWithEmbeddedDataOnly") + + # The copyright is hex encoded in the source; rewrite the whole + # LocalizedText element in the CDATA form used by the description + m = re.search(r'(.*?)' + r'(.*?)', text, re.DOTALL) + if m is None: + fail("could not locate the copyrightTag LocalizedText element") + text = (text[:m.start()] + + m.group(1) + + '' + '' + + m.group(2) + + text[m.end():]) + + text = replace_exactly(text, + "", + "", + 1, "profileDescriptionTag text") + return text + + +def reduce_cluts(text): + report = [] + + # OutputChannels for a bare comes from the nearest preceding + # of its enclosing lutAtoBType / lutBtoAType + channels = [(m.start(), int(m.group(2))) for m in CHANNELS_RE.finditer(text)] + + def out_before(pos): + best = None + for start, out in channels: + if start < pos: + best = out + else: + break + if best is None: + fail("no element precedes the CLUT at offset %d" % pos) + return best + + def target_for(grid): + new_g = TARGET_BY_INPUTS.get(len(grid)) + if new_g is None: + fail("no target grid defined for %d input channels" % len(grid)) + return new_g + + def do_clut(m): + grid = [int(x) for x in m.group(1).split()] + tokens = m.group(2).split() + out = out_before(m.start()) + new_g = target_for(grid) + points, stride = subsample(tokens, grid, out, new_g) + report.append(("CLUT", len(grid), out, grid[0], new_g, stride, len(points))) + return ('\n' + ' %s\n' + ' \n%s\n' + ' \n' + ' ' + % (" ".join([str(new_g)] * len(grid)), render(points, " "))) + + def do_extclut(m): + opening = m.group(1) + grid = [int(x) for x in m.group(2).split()] + tokens = m.group(3).split() + mo = OUT_ATTR_RE.search(opening) + if mo is None: + fail("ExtCLutElement has no OutputChannels attribute") + out = int(mo.group(1)) + new_g = target_for(grid) + points, stride = subsample(tokens, grid, out, new_g) + report.append(("ExtCLutElement", len(grid), out, grid[0], new_g, stride, len(points))) + return ('%s\n' + ' %s\n' + ' \n%s\n' + ' ' + % (opening, " ".join([str(new_g)] * len(grid)), + render(points, " "))) + + # first, scanning the original text so out_before offsets stay valid + text = CLUT_RE.sub(do_clut, text) + text = EXTCLUT_RE.sub(do_extclut, text) + return text, report + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: subsample_clut.py ") + + src, dst = sys.argv[1], sys.argv[2] + + with open(src, encoding="utf-8") as f: + text = f.read() + + text = edit_metadata(text) + text, report = reduce_cluts(text) + + if not report: + fail("no CLUTs were found, nothing was reduced") + + # HybridPrinterCMYK.icc has exactly this many reducible tables: a partial regex + # match that quietly skipped some would otherwise produce a fixture that still + # carries full size CLUTs + if len(report) != EXPECTED_CLUTS: + fail("reduced %d CLUTs, expected %d - the regexes probably matched partially" + % (len(report), EXPECTED_CLUTS)) + + with open(dst, "w", encoding="utf-8", newline="\n") as f: + f.write(text) + + print("%-16s %6s %5s %8s %8s %7s %8s" + % ("element", "inputs", "out", "oldgrid", "newgrid", "stride", "points")) + for row in report: + print("%-16s %6d %5d %8d %8d %7d %8d" % row) + print("\nwrote %s" % dst) + + +if __name__ == "__main__": + main() diff --git a/testbed/testcms2.c b/testbed/testcms2.c index 6e43f7272..861b6c7de 100644 --- a/testbed/testcms2.c +++ b/testbed/testcms2.c @@ -9603,6 +9603,2891 @@ void SpeedTest(void) } +// A CLUT whose total sample count does not fit in 32 bits must be refused at +// allocation time rather than allocated short. CubeSize() bounds only its own product, +// so the multiply by the output channel count is where it overflows: the grid below is +// 11*31*63*63*129 = 174592341 nodes, and times 123 output channels that wraps to +// 21463. The allocation would be 84 kB while _cmsComputeInterpParamsEx computed full +// width strides for it, so the first cmsPipelineEvalFloat read far out of bounds. +// +// Deliberately outside CMS_USE_ICCMAX_SPECTRAL: the allocator guard is unconditional. +static +cmsInt32Number CheckCLUTOverflowRejected(void) +{ + cmsUInt32Number Overflowing[5] = { 11, 31, 63, 63, 129 }; + cmsUInt32Number Small[3] = { 2, 2, 2 }; + cmsStage* mpe; + + mpe = cmsStageAllocCLutFloatGranular(DbgThread(), Overflowing, 5, 123, NULL); + if (mpe != NULL) { + + Fail("cmsStageAllocCLutFloatGranular accepted a grid whose sample count overflows"); + cmsStageFree(mpe); + return 0; + } + + // The same check must not turn away an ordinary table + mpe = cmsStageAllocCLutFloatGranular(DbgThread(), Small, 3, 3, NULL); + if (mpe == NULL) { + + Fail("cmsStageAllocCLutFloatGranular refused a 2x2x2 to 3 CLUT"); + return 0; + } + + cmsStageFree(mpe); + return 1; +} + + +// ----------------------------------------------------------------------------------------------------- +// iccMAX (ICC.2) support: extendedCLUTElement, spectral PCS, embedded ICC.2 profiles +// ----------------------------------------------------------------------------------------------------- + +#ifdef CMS_USE_ICCMAX_SPECTRAL + + +// The conversion maths is already covered exhaustively by CheckFormattersHalf, which +// walks all 65536 half patterns. What is checked here is the IO layer added for +// iccMAX: byte order and handler plumbing. Wire bytes are given explicitly, since a +// byte swapped reader and a byte swapped writer would cancel out in a round trip. +static +cmsInt32Number CheckFloat16IO(void) +{ + typedef struct { cmsUInt8Number hi, lo; cmsFloat32Number value; } f16case; + + static const f16case cases[] = { + { 0x00, 0x00, 0.0f }, // +0 + { 0x80, 0x00, -0.0f }, // -0 + { 0x3C, 0x00, 1.0f }, + { 0xBC, 0x00, -1.0f }, + { 0x38, 0x00, 0.5f }, + { 0x7B, 0xFF, 65504.0f }, // largest normal + { 0x5C, 0x00, 256.0f }, + { 0x5D, 0xF0, 380.0f }, // wavelengths of the kind spectralRange carries + { 0x61, 0xB4, 730.0f } + }; + + cmsIOHANDLER* io; + cmsUInt32Number i, n = sizeof(cases) / sizeof(cases[0]); + cmsFloat32Number v; + cmsUInt8Number buf[2]; + cmsBool rc; + + // Read path: exact bytes in, expected value out + for (i = 0; i < n; i++) { + + buf[0] = cases[i].hi; + buf[1] = cases[i].lo; + + io = cmsOpenIOhandlerFromMem(DbgThread(), buf, 2, "r"); + if (io == NULL) { Fail("Cannot open mem IO for reading"); return 0; } + + v = -12345.0f; + if (!_cmsReadFloat16Number(io, &v)) { + Fail("_cmsReadFloat16Number failed on case %d", i); + cmsCloseIOhandler(io); + return 0; + } + cmsCloseIOhandler(io); + + if (v != cases[i].value) { + Fail("_cmsReadFloat16Number case %d: got %f, expected %f", i, v, cases[i].value); + return 0; + } + } + + // Write path: value in, exact bytes out + for (i = 0; i < n; i++) { + + memset(buf, 0xAA, sizeof(buf)); + + io = cmsOpenIOhandlerFromMem(DbgThread(), buf, 2, "w"); + if (io == NULL) { Fail("Cannot open mem IO for writing"); return 0; } + + if (!_cmsWriteFloat16Number(io, cases[i].value)) { + Fail("_cmsWriteFloat16Number failed on case %d", i); + cmsCloseIOhandler(io); + return 0; + } + cmsCloseIOhandler(io); + + if (buf[0] != cases[i].hi || buf[1] != cases[i].lo) { + Fail("_cmsWriteFloat16Number case %d: got %02X %02X, expected %02X %02X", + i, buf[0], buf[1], cases[i].hi, cases[i].lo); + return 0; + } + } + + // A truncated buffer must fail rather than read past the end. This is expected to + // signal an IO error, so trap it instead of letting it fail the test. + io = cmsOpenIOhandlerFromMem(DbgThread(), buf, 1, "r"); + if (io == NULL) { Fail("Cannot open mem IO for short read"); return 0; } + + cmsSetLogErrorHandler(ErrorReportingFunction); + rc = _cmsReadFloat16Number(io, &v); + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + + cmsCloseIOhandler(io); + + if (rc) { + Fail("_cmsReadFloat16Number should fail on a 1 byte buffer"); + return 0; + } + + return 1; +} + + +// Builds a CMYK to 37 channel spectral pipeline, stores it as a DToB3 tag in a +// profile carrying a spectral PCS, then reads it back and evaluates it. Exercises +// the extendedCLUT writer and reader together with the header fields. +static +cmsInt32Number CheckExtCLutElement(void) +{ + cmsHPROFILE h = NULL; + cmsPipeline* pipe = NULL; + cmsPipeline* ReadPipe; + cmsStage* clut; + cmsUInt32Number GridPoints[4] = { 3, 3, 3, 3 }; + cmsUInt32Number i, clen = 0; + cmsUInt32Number rc = 0; + char* data = NULL; + cmsFloat32Number In[4], Out[37]; + _cmsStageCLutData* ClutData; + + pipe = cmsPipelineAlloc(DbgThread(), 4, 37); + if (pipe == NULL) { Fail("cmsPipelineAlloc 4->37 failed"); return 0; } + + clut = cmsStageAllocCLutFloatGranular(DbgThread(), GridPoints, 4, 37, NULL); + if (clut == NULL) { Fail("cmsStageAllocCLutFloatGranular 4->37 failed"); goto Error; } + + // Fill with a pattern that varies across the whole table + ClutData = (_cmsStageCLutData*) clut ->Data; + for (i = 0; i < ClutData ->nEntries; i++) + ClutData ->Tab.TFloat[i] = (cmsFloat32Number) i / (cmsFloat32Number) ClutData ->nEntries; + + // Ask for it to be stored as an extendedCLUT rather than an ICC.1 clut + clut ->Type = (cmsStageSignature) cmsSigExtCLutElemType; + + if (!cmsPipelineInsertStage(pipe, cmsAT_END, clut)) { + Fail("cmsPipelineInsertStage failed"); + cmsStageFree(clut); + goto Error; + } + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); goto Error; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigCmykData); + cmsSetPCS(h, cmsSigLabData); + + // 'rs' with 37 channels, i.e. reflectance spectra, 380 to 730nm in 37 steps + if (!cmsSetSpectralPCS(h, 0x72730025)) { + Fail("cmsSetSpectralPCS failed"); + goto Error; + } + if (!cmsSetSpectralPCSRange(h, 380.0f, 730.0f, 37)) { + Fail("cmsSetSpectralPCSRange failed"); + goto Error; + } + + if (!cmsWriteTag(h, cmsSigDToB3Tag, pipe)) { Fail("cmsWriteTag DToB3 failed"); goto Error; } + + if (!cmsSaveProfileToMem(h, NULL, &clen)) { Fail("cmsSaveProfileToMem size failed"); goto Error; } + + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); goto Error; } + + if (!cmsSaveProfileToMem(h, data, &clen)) { Fail("cmsSaveProfileToMem failed"); goto Error; } + + cmsCloseProfile(h); h = NULL; + cmsPipelineFree(pipe); pipe = NULL; + + h = cmsOpenProfileFromMem(data, clen); + if (h == NULL) { Fail("cmsOpenProfileFromMem failed"); goto Error; } + + if (cmsGetSpectralPCS(h) != 0x72730025) { + Fail("SpectralPCS: got 0x%x, expected 0x72730025", cmsGetSpectralPCS(h)); + goto Error; + } + + if (cmsGetSpectralPCSChannels(h) != 37) { + Fail("Spectral channels: got %d, expected 37", cmsGetSpectralPCSChannels(h)); + goto Error; + } + + ReadPipe = (cmsPipeline*) cmsReadTag(h, cmsSigDToB3Tag); + if (ReadPipe == NULL) { Fail("cmsReadTag DToB3 returned NULL"); goto Error; } + + if (cmsPipelineInputChannels(ReadPipe) != 4) { + Fail("Input channels: got %d, expected 4", cmsPipelineInputChannels(ReadPipe)); + goto Error; + } + + if (cmsPipelineOutputChannels(ReadPipe) != 37) { + Fail("Output channels: got %d, expected 37", cmsPipelineOutputChannels(ReadPipe)); + goto Error; + } + + // The element must come back tagged as an extendedCLUT, not downgraded to 'clut' + if (cmsStageType(cmsPipelineGetPtrToFirstStage(ReadPipe)) != (cmsStageSignature) cmsSigExtCLutElemType) { + Fail("First stage is not an extendedCLUT after round trip"); + goto Error; + } + + // At the origin of the grid the output is the first 37 table entries + In[0] = 0.0f; In[1] = 0.0f; In[2] = 0.0f; In[3] = 0.0f; + cmsPipelineEvalFloat(In, Out, ReadPipe); + + ClutData = (_cmsStageCLutData*) cmsPipelineGetPtrToFirstStage(ReadPipe) ->Data; + + for (i = 0; i < 37; i++) { + + cmsFloat32Number expected = (cmsFloat32Number) i / (cmsFloat32Number) ClutData ->nEntries; + + if (fabs(Out[i] - expected) > 1e-5) { + Fail("extCLUT output[%d]: got %f, expected %f", i, Out[i], expected); + goto Error; + } + } + + rc = 1; + +Error: + if (h != NULL) cmsCloseProfile(h); + if (pipe != NULL) cmsPipelineFree(pipe); + if (data != NULL) free(data); + return rc; +} + + +// Reads an extendedCLUT in a given valueEncodingType. Since the writer always emits +// float32, the other three encodings can only be reached by handing the reader raw +// wire bytes, which is what this does: a hand assembled multiProcessElementsType +// holding one 1 in, 1 out, 2 grid point extendedCLUT. +// +// Returns 1 when the element read back and interpolated correctly. A refusal to read +// returns 0 without reporting anything, since one caller wants exactly that; a value +// that reads but comes back wrong always reports, since that is a bug either way. +static +cmsInt32Number TryExtCLutEncoding(cmsUInt32Number EncodingType, + cmsBool AsUInt32, + const cmsUInt8Number* ClutBytes, + cmsUInt32Number ClutBytesSize) +{ + cmsUInt8Number raw[128]; + cmsUInt32Number pos = 0; + cmsUInt32Number ElemOffset, ElemSize; + cmsHPROFILE h; + cmsHPROFILE h2; + cmsPipeline* pipe; + cmsFloat32Number In[1], Out[1]; + cmsUInt32Number clen = 0; + char* data; + + memset(raw, 0, sizeof(raw)); + + // multiProcessElementsType header. The 'mpet' signature and its 4 reserved bytes + // are the tag base, which cmsWriteRawTag stores verbatim. + raw[pos++] = 'm'; raw[pos++] = 'p'; raw[pos++] = 'e'; raw[pos++] = 't'; + pos += 4; // reserved + raw[pos++] = 0; raw[pos++] = 1; // input channels + raw[pos++] = 0; raw[pos++] = 1; // output channels + raw[pos++] = 0; raw[pos++] = 0; + raw[pos++] = 0; raw[pos++] = 1; // element count + + // Position table, one entry: offset from the start of the tag, then size + ElemOffset = pos + 8; + ElemSize = 4 + 4 + 2 + 2 + 4 + 16 + ClutBytesSize; + + raw[pos++] = (cmsUInt8Number)(ElemOffset >> 24); raw[pos++] = (cmsUInt8Number)(ElemOffset >> 16); + raw[pos++] = (cmsUInt8Number)(ElemOffset >> 8); raw[pos++] = (cmsUInt8Number)(ElemOffset); + raw[pos++] = (cmsUInt8Number)(ElemSize >> 24); raw[pos++] = (cmsUInt8Number)(ElemSize >> 16); + raw[pos++] = (cmsUInt8Number)(ElemSize >> 8); raw[pos++] = (cmsUInt8Number)(ElemSize); + + // The extendedCLUT element itself, ICC.2:2023 Table 117 + raw[pos++] = 'x'; raw[pos++] = 'c'; raw[pos++] = 'l'; raw[pos++] = 't'; + pos += 4; // reserved(3) + interpolation hint(1) + raw[pos++] = 0; raw[pos++] = 1; // input channels (P) + raw[pos++] = 0; raw[pos++] = 1; // output channels (Q) + + // The encoding type. Real profiles and the reference implementation write a + // uInt16Number here plus 2 reserved bytes; AsUInt32 asks for the literal reading + // of Table 117 instead, which the reader also has to accept. + if (AsUInt32) { + raw[pos++] = (cmsUInt8Number)(EncodingType >> 24); raw[pos++] = (cmsUInt8Number)(EncodingType >> 16); + raw[pos++] = (cmsUInt8Number)(EncodingType >> 8); raw[pos++] = (cmsUInt8Number)(EncodingType); + } + else { + raw[pos++] = (cmsUInt8Number)(EncodingType >> 8); raw[pos++] = (cmsUInt8Number)(EncodingType); + raw[pos++] = 0; raw[pos++] = 0; // reserved + } + + raw[pos++] = 2; // 2 grid points on the first axis + pos += 15; // the other 15 stay zero + + if (pos + ClutBytesSize > sizeof(raw)) { Fail("raw buffer too small"); return 0; } + memmove(raw + pos, ClutBytes, ClutBytesSize); + pos += ClutBytesSize; + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); return 0; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigGrayData); + cmsSetPCS(h, cmsSigLabData); + + if (!cmsWriteRawTag(h, cmsSigDToB3Tag, raw, pos)) { + Fail("cmsWriteRawTag failed for encoding %d", EncodingType); + cmsCloseProfile(h); + return 0; + } + + if (!cmsSaveProfileToMem(h, NULL, &clen)) { + Fail("cmsSaveProfileToMem size failed for encoding %d", EncodingType); + cmsCloseProfile(h); + return 0; + } + + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); cmsCloseProfile(h); return 0; } + + if (!cmsSaveProfileToMem(h, data, &clen)) { + Fail("cmsSaveProfileToMem failed for encoding %d", EncodingType); + cmsCloseProfile(h); + free(data); + return 0; + } + cmsCloseProfile(h); + + h2 = cmsOpenProfileFromMem(data, clen); + free(data); + if (h2 == NULL) { Fail("Cannot reopen profile for encoding %d", EncodingType); return 0; } + + // Not reported: the caller decides whether a refusal is the right answer + pipe = (cmsPipeline*) cmsReadTag(h2, cmsSigDToB3Tag); + if (pipe == NULL) { cmsCloseProfile(h2); return 0; } + + // The two grid points hold 0.0 and 1.0, so the ends of the input range read them back + In[0] = 0.0f; + cmsPipelineEvalFloat(In, Out, pipe); + if (fabs(Out[0]) > 1e-5) { + Fail("Encoding %d at input 0.0: got %f, expected 0.0", EncodingType, Out[0]); + cmsCloseProfile(h2); + return 0; + } + + In[0] = 1.0f; + cmsPipelineEvalFloat(In, Out, pipe); + if (fabs(Out[0] - 1.0f) > 1e-5) { + Fail("Encoding %d at input 1.0: got %f, expected 1.0", EncodingType, Out[0]); + cmsCloseProfile(h2); + return 0; + } + + // Halfway between them must interpolate + In[0] = 0.5f; + cmsPipelineEvalFloat(In, Out, pipe); + if (fabs(Out[0] - 0.5f) > 1e-4) { + Fail("Encoding %d at input 0.5: got %f, expected 0.5", EncodingType, Out[0]); + cmsCloseProfile(h2); + return 0; + } + + cmsCloseProfile(h2); + return 1; +} + +static +cmsInt32Number CheckExtCLutAllEncodings(void) +{ + static const cmsUInt8Number Float32Data[] = { + 0x00, 0x00, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x00 + }; + static const cmsUInt8Number Float16Data[] = { + 0x00, 0x00, + 0x3C, 0x00 // 1.0 as float16 + }; + static const cmsUInt8Number UInt16Data[] = { + 0x00, 0x00, + 0xFF, 0xFF + }; + static const cmsUInt8Number UInt8Data[] = { + 0x00, + 0xFF + }; + + // Each encoding, with the field written the way real profiles write it + if (!TryExtCLutEncoding(0, FALSE, Float32Data, sizeof(Float32Data))) { + Fail("float32 extendedCLUT failed"); + return 0; + } + + if (!TryExtCLutEncoding(1, FALSE, Float16Data, sizeof(Float16Data))) { + Fail("float16 extendedCLUT failed"); + return 0; + } + + if (!TryExtCLutEncoding(2, FALSE, UInt16Data, sizeof(UInt16Data))) { + Fail("uInt16 extendedCLUT failed"); + return 0; + } + + if (!TryExtCLutEncoding(3, FALSE, UInt8Data, sizeof(UInt8Data))) { + Fail("uInt8 extendedCLUT failed"); + return 0; + } + + // And again with the field spelled as a uInt32Number, the literal reading of + // Table 117, which must also be understood + if (!TryExtCLutEncoding(0, TRUE, Float32Data, sizeof(Float32Data))) { + Fail("float32 extendedCLUT failed with a uInt32 encoding field"); + return 0; + } + + if (!TryExtCLutEncoding(2, TRUE, UInt16Data, sizeof(UInt16Data))) { + Fail("uInt16 extendedCLUT failed with a uInt32 encoding field"); + return 0; + } + + if (!TryExtCLutEncoding(3, TRUE, UInt8Data, sizeof(UInt8Data))) { + Fail("uInt8 extendedCLUT failed with a uInt32 encoding field"); + return 0; + } + + // An unknown encoding type must be rejected, not guessed at + { + cmsInt32Number rc; + + cmsSetLogErrorHandler(ErrorReportingFunction); + rc = TryExtCLutEncoding(99, FALSE, Float32Data, sizeof(Float32Data)); + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + + if (rc) { + Fail("An unknown valueEncodingType was accepted"); + return 0; + } + + if (SimultaneousErrors == 0) { + Fail("An unknown valueEncodingType was rejected without reporting why"); + return 0; + } + SimultaneousErrors = 0; + } + + return 1; +} + + +// Pipelines wider than the old cmsMAXCHANNELS ceiling must allocate and evaluate. +// Only direct evaluation is exercised: transforms still use cmsMAXCHANNELS buffers. +static +cmsInt32Number CheckHighChannelPipeline(void) +{ + cmsPipeline* pipe; + cmsStage* mpe; + _cmsStageCLutData* clut; + cmsFloat32Number In[3], Out[64]; + cmsUInt32Number GridPoints[3] = { 2, 2, 2 }; + cmsUInt32Number i; + cmsUInt32Number rc = 0; + + pipe = cmsPipelineAlloc(DbgThread(), 3, 64); + if (pipe == NULL) { Fail("cmsPipelineAlloc(3,64) failed, channel limit too low?"); return 0; } + + mpe = cmsStageAllocCLutFloatGranular(DbgThread(), GridPoints, 3, 64, NULL); + if (mpe == NULL) { Fail("cmsStageAllocCLutFloatGranular 3->64 failed"); goto Error; } + + // Zero everywhere, then give the first grid point a ramp across all 64 outputs + clut = (_cmsStageCLutData*) mpe ->Data; + for (i = 0; i < clut ->nEntries; i++) + clut ->Tab.TFloat[i] = 0.0f; + + for (i = 0; i < 64; i++) + clut ->Tab.TFloat[i] = (cmsFloat32Number) i / 63.0f; + + if (!cmsPipelineInsertStage(pipe, cmsAT_END, mpe)) { + Fail("cmsPipelineInsertStage failed"); + cmsStageFree(mpe); + goto Error; + } + + In[0] = 0.0f; In[1] = 0.0f; In[2] = 0.0f; + cmsPipelineEvalFloat(In, Out, pipe); + + for (i = 0; i < 64; i++) { + + cmsFloat32Number expected = (cmsFloat32Number) i / 63.0f; + + if (fabs(Out[i] - expected) > 1e-5) { + Fail("High channel output[%d]: got %f, expected %f", i, Out[i], expected); + goto Error; + } + } + + // MAX_STAGE_CHANNELS itself is still out of bounds + { + cmsPipeline* TooWide = cmsPipelineAlloc(DbgThread(), 3, MAX_STAGE_CHANNELS); + if (TooWide != NULL) { + Fail("cmsPipelineAlloc accepted MAX_STAGE_CHANNELS outputs"); + cmsPipelineFree(TooWide); + goto Error; + } + } + + rc = 1; + +Error: + cmsPipelineFree(pipe); + return rc; +} + + +// Opening and re-saving an ordinary ICC.1 profile must not invent a spectral PCS, +// and a profile that has one must round trip it. +static +cmsInt32Number CheckSpectralPCSPreservation(void) +{ + cmsHPROFILE h; + cmsUInt32Number clen = 0; + char* data; + cmsFloat32Number Start, End; + cmsUInt16Number Steps; + + // A vanilla v4 profile + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); return 0; } + + cmsSetProfileVersion(h, 4.3); + cmsSetDeviceClass(h, cmsSigDisplayClass); + cmsSetColorSpace(h, cmsSigRgbData); + cmsSetPCS(h, cmsSigXYZData); + + if (cmsGetSpectralPCS(h) != 0) { + Fail("A new profile should have no spectral PCS"); + cmsCloseProfile(h); + return 0; + } + + // The range setter must refuse to run without a signature + if (cmsSetSpectralPCSRange(h, 380.0f, 730.0f, 37)) { + Fail("cmsSetSpectralPCSRange should fail with no spectral PCS set"); + cmsCloseProfile(h); + return 0; + } + + if (cmsGetSpectralPCSRange(h, &Start, &End, &Steps)) { + Fail("cmsGetSpectralPCSRange should fail with no spectral PCS set"); + cmsCloseProfile(h); + return 0; + } + + cmsSaveProfileToMem(h, NULL, &clen); + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); cmsCloseProfile(h); return 0; } + cmsSaveProfileToMem(h, data, &clen); + cmsCloseProfile(h); + + h = cmsOpenProfileFromMem(data, clen); + free(data); + if (h == NULL) { Fail("Cannot reopen the vanilla profile"); return 0; } + + if (cmsGetSpectralPCS(h) != 0) { + Fail("Round trip invented a spectral PCS: 0x%x", cmsGetSpectralPCS(h)); + cmsCloseProfile(h); + return 0; + } + + if (cmsGetSpectralPCSChannels(h) != 0) { + Fail("Vanilla profile reports %d spectral channels", cmsGetSpectralPCSChannels(h)); + cmsCloseProfile(h); + return 0; + } + cmsCloseProfile(h); + + // Now one that does carry a spectral PCS + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); return 0; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigCmykData); + cmsSetPCS(h, cmsSigLabData); + + if (!cmsSetSpectralPCS(h, 0x72730025)) { // 'rs' with 37 channels + Fail("cmsSetSpectralPCS failed"); + cmsCloseProfile(h); + return 0; + } + if (!cmsSetSpectralPCSRange(h, 380.0f, 730.0f, 37)) { + Fail("cmsSetSpectralPCSRange failed"); + cmsCloseProfile(h); + return 0; + } + + clen = 0; + cmsSaveProfileToMem(h, NULL, &clen); + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); cmsCloseProfile(h); return 0; } + cmsSaveProfileToMem(h, data, &clen); + cmsCloseProfile(h); + + h = cmsOpenProfileFromMem(data, clen); + free(data); + if (h == NULL) { Fail("Cannot reopen the spectral profile"); return 0; } + + if (cmsGetSpectralPCS(h) != 0x72730025) { + Fail("SpectralPCS: got 0x%x, expected 0x72730025", cmsGetSpectralPCS(h)); + cmsCloseProfile(h); + return 0; + } + + if (cmsGetSpectralPCSChannels(h) != 37) { + Fail("Spectral channels: got %d, expected 37", cmsGetSpectralPCSChannels(h)); + cmsCloseProfile(h); + return 0; + } + + if (!cmsGetSpectralPCSRange(h, &Start, &End, &Steps)) { + Fail("cmsGetSpectralPCSRange returned FALSE"); + cmsCloseProfile(h); + return 0; + } + + // 380 and 730 are both exactly representable as float16 + if (Start != 380.0f || End != 730.0f || Steps != 37) { + Fail("Spectral range: got start=%f end=%f steps=%d", Start, End, Steps); + cmsCloseProfile(h); + return 0; + } + + // The PCS itself must be untouched by all of this + if (cmsGetPCS(h) != cmsSigLabData) { + Fail("The PCS field was corrupted by the spectral PCS write"); + cmsCloseProfile(h); + return 0; + } + + cmsCloseProfile(h); + return 1; +} + + +// Builds a DToB3 holding a curveSetElement of one singleSampledCurve, from raw wire +// bytes, then reads it back and evaluates it at the given inputs. There is no API for +// authoring an 'sngf' from scratch, and the real profile only ever uses N=2, uInt16, +// clip, so the encodings and the extrapolating mode need driving by hand. +// +// Samples are given already encoded. nEntries, F and L describe them; Extension is 0 +// for clip or 1 for linear extrapolation. +static +cmsInt32Number TrySingleSampledCurve(cmsUInt32Number nEntries, + cmsFloat32Number F, cmsFloat32Number L, + cmsUInt16Number Extension, + cmsUInt16Number EncodingType, + const cmsUInt8Number* SampleBytes, + cmsUInt32Number SampleBytesSize, + const cmsFloat32Number* In, + const cmsFloat32Number* Expected, + cmsUInt32Number nProbes, + cmsFloat32Number Tolerance) +{ + cmsUInt8Number raw[256]; + cmsUInt32Number pos = 0; + cmsUInt32Number ElemOffset, ElemSize, CurveOffset, CurveSize; + cmsHPROFILE h, h2; + cmsPipeline* pipe; + cmsUInt32Number clen = 0, i; + char* data; + union { cmsFloat32Number f; cmsUInt32Number u; } conv; + +#define PUT32(v) do { cmsUInt32Number t_ = (v); \ + raw[pos++] = (cmsUInt8Number)(t_ >> 24); raw[pos++] = (cmsUInt8Number)(t_ >> 16); \ + raw[pos++] = (cmsUInt8Number)(t_ >> 8); raw[pos++] = (cmsUInt8Number)(t_); } while (0) +#define PUT16(v) do { cmsUInt16Number t_ = (v); \ + raw[pos++] = (cmsUInt8Number)(t_ >> 8); raw[pos++] = (cmsUInt8Number)(t_); } while (0) + + memset(raw, 0, sizeof(raw)); + + CurveSize = 24 + SampleBytesSize; + ElemSize = 12 + 8 + CurveSize; // header + one position table entry + curve + CurveOffset = 20; // from the start of the element + + // multiProcessElementsType, 1 in 1 out, one element + raw[pos++] = 'm'; raw[pos++] = 'p'; raw[pos++] = 'e'; raw[pos++] = 't'; + PUT32(0); + PUT16(1); + PUT16(1); + PUT32(1); + + ElemOffset = pos + 8; + PUT32(ElemOffset); + PUT32(ElemSize); + + // curveSetElement: signature, reserved, channels, then a position table + raw[pos++] = 'c'; raw[pos++] = 'v'; raw[pos++] = 's'; raw[pos++] = 't'; + PUT32(0); + PUT16(1); + PUT16(1); + PUT32(CurveOffset); + PUT32(CurveSize); + + // The singleSampledCurve, ICC.2:2023 Table 108 + raw[pos++] = 's'; raw[pos++] = 'n'; raw[pos++] = 'g'; raw[pos++] = 'f'; + PUT32(0); + PUT32(nEntries); + conv.f = F; PUT32(conv.u); + conv.f = L; PUT32(conv.u); + PUT16(Extension); + PUT16(EncodingType); + + if (pos + SampleBytesSize > sizeof(raw)) { Fail("raw buffer too small"); return 0; } + memmove(raw + pos, SampleBytes, SampleBytesSize); + pos += SampleBytesSize; + +#undef PUT32 +#undef PUT16 + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); return 0; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigGrayData); + cmsSetPCS(h, cmsSigLabData); + + if (!cmsWriteRawTag(h, cmsSigDToB3Tag, raw, pos)) { + Fail("cmsWriteRawTag failed"); + cmsCloseProfile(h); + return 0; + } + + cmsSaveProfileToMem(h, NULL, &clen); + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); cmsCloseProfile(h); return 0; } + cmsSaveProfileToMem(h, data, &clen); + cmsCloseProfile(h); + + h2 = cmsOpenProfileFromMem(data, clen); + free(data); + if (h2 == NULL) { Fail("Cannot reopen the profile"); return 0; } + + // Not reported: a caller may be checking that a malformed curve is refused + pipe = (cmsPipeline*) cmsReadTag(h2, cmsSigDToB3Tag); + if (pipe == NULL) { cmsCloseProfile(h2); return 0; } + + for (i = 0; i < nProbes; i++) { + + cmsFloat32Number vin = In[i], vout = -12345.0f; + + cmsPipelineEvalFloat(&vin, &vout, pipe); + + if (fabs(vout - Expected[i]) > Tolerance) { + Fail("sngf(enc %d, ext %d) at %f: got %f, expected %f", + EncodingType, Extension, vin, vout, Expected[i]); + cmsCloseProfile(h2); + return 0; + } + } + + cmsCloseProfile(h2); + return 1; +} + + +static +cmsInt32Number CheckSingleSampledCurve(void) +{ + // A five entry ramp over [0, 1]: 0, 0.25, 0.5, 0.75, 1 + static const cmsUInt8Number RampF32[] = { + 0x00, 0x00, 0x00, 0x00, // 0.0 + 0x3E, 0x80, 0x00, 0x00, // 0.25 + 0x3F, 0x00, 0x00, 0x00, // 0.5 + 0x3F, 0x40, 0x00, 0x00, // 0.75 + 0x3F, 0x80, 0x00, 0x00 // 1.0 + }; + static const cmsUInt8Number RampU16[] = { + 0x00, 0x00, + 0x40, 0x00, // 16384/65535 = 0.2500 + 0x80, 0x00, // 32768/65535 = 0.5000 + 0xBF, 0xFF, + 0xFF, 0xFF + }; + static const cmsUInt8Number RampU8[] = { 0x00, 0x40, 0x80, 0xBF, 0xFF }; + static const cmsUInt8Number RampF16[] = { + 0x00, 0x00, // 0.0 + 0x34, 0x00, // 0.25 + 0x38, 0x00, // 0.5 + 0x3A, 0x00, // 0.75 + 0x3C, 0x00 // 1.0 + }; + + // Identity inside the domain, and each encoding must land on the same answers + static const cmsFloat32Number In[] = { 0.0f, 0.125f, 0.25f, 0.5f, 0.875f, 1.0f }; + static const cmsFloat32Number Out[] = { 0.0f, 0.125f, 0.25f, 0.5f, 0.875f, 1.0f }; + + if (!TrySingleSampledCurve(5, 0.0f, 1.0f, 0, 0, RampF32, sizeof(RampF32), + In, Out, 6, 1e-5f)) { + Fail("float32 singleSampledCurve failed"); + return 0; + } + + if (!TrySingleSampledCurve(5, 0.0f, 1.0f, 0, 2, RampU16, sizeof(RampU16), + In, Out, 6, 1e-4f)) { + Fail("uInt16 singleSampledCurve failed"); + return 0; + } + + // uInt8 quantises 0.25 to 64/255, so allow the coarser step + if (!TrySingleSampledCurve(5, 0.0f, 1.0f, 0, 3, RampU8, sizeof(RampU8), + In, Out, 6, 4e-3f)) { + Fail("uInt8 singleSampledCurve failed"); + return 0; + } + + if (!TrySingleSampledCurve(5, 0.0f, 1.0f, 0, 1, RampF16, sizeof(RampF16), + In, Out, 6, 1e-4f)) { + Fail("float16 singleSampledCurve failed"); + return 0; + } + + // Clipping outside [F, L]. Domain is [0.25, 0.75], so 0 clips to the first sample + // and 1 clips to the last. + { + static const cmsFloat32Number ClipIn[] = { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f }; + static const cmsFloat32Number ClipOut[] = { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f }; + + if (!TrySingleSampledCurve(5, 0.25f, 0.75f, 0, 0, RampF32, sizeof(RampF32), + ClipIn, ClipOut, 5, 1e-5f)) { + Fail("clipping singleSampledCurve failed"); + return 0; + } + } + + // Linear extrapolation over the same domain. The ramp has slope 2 in this domain, + // so below 0.25 it continues down to -0.5 at 0 and above 0.75 up to 1.5 at 1. + { + static const cmsFloat32Number ExtIn[] = { 0.0f, 0.125f, 0.25f, 0.5f, 0.75f, 0.875f, 1.0f }; + static const cmsFloat32Number ExtOut[] = { -0.5f, -0.25f, 0.0f, 0.5f, 1.0f, 1.25f, 1.5f }; + + if (!TrySingleSampledCurve(5, 0.25f, 0.75f, 1, 0, RampF32, sizeof(RampF32), + ExtIn, ExtOut, 7, 1e-5f)) { + Fail("extrapolating singleSampledCurve failed"); + return 0; + } + } + + // Two entries is the minimum and must work + { + static const cmsUInt8Number TwoPoint[] = { + 0x3E, 0x80, 0x00, 0x00, // 0.25 + 0x3F, 0x40, 0x00, 0x00 // 0.75 + }; + static const cmsFloat32Number TwoIn[] = { 0.0f, 0.5f, 1.0f }; + static const cmsFloat32Number TwoOut[] = { 0.25f, 0.5f, 0.75f }; + + if (!TrySingleSampledCurve(2, 0.0f, 1.0f, 0, 0, TwoPoint, sizeof(TwoPoint), + TwoIn, TwoOut, 3, 1e-5f)) { + Fail("two entry singleSampledCurve failed"); + return 0; + } + } + + // Malformed curves must be refused rather than half read + { + static const cmsFloat32Number Dummy[] = { 0.0f }; + cmsInt32Number rc; + + cmsSetLogErrorHandler(ErrorReportingFunction); + + // Fewer than two entries + rc = TrySingleSampledCurve(1, 0.0f, 1.0f, 0, 0, RampF32, 4, Dummy, Dummy, 0, 1e-5f); + if (rc) { Fail("A one entry singleSampledCurve was accepted"); goto TrapError; } + + // L must be greater than F + rc = TrySingleSampledCurve(5, 1.0f, 1.0f, 0, 0, RampF32, sizeof(RampF32), Dummy, Dummy, 0, 1e-5f); + if (rc) { Fail("A singleSampledCurve with F equal to L was accepted"); goto TrapError; } + + rc = TrySingleSampledCurve(5, 1.0f, 0.0f, 0, 0, RampF32, sizeof(RampF32), Dummy, Dummy, 0, 1e-5f); + if (rc) { Fail("A singleSampledCurve with L below F was accepted"); goto TrapError; } + + // Unknown extension type + rc = TrySingleSampledCurve(5, 0.0f, 1.0f, 7, 0, RampF32, sizeof(RampF32), Dummy, Dummy, 0, 1e-5f); + if (rc) { Fail("An unknown lookup extension type was accepted"); goto TrapError; } + + // Unknown value encoding + rc = TrySingleSampledCurve(5, 0.0f, 1.0f, 0, 42, RampF32, sizeof(RampF32), Dummy, Dummy, 0, 1e-5f); + if (rc) { Fail("An unknown value encoding type was accepted"); goto TrapError; } + + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + goto Continue; + + TrapError: + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + return 0; + } + +Continue: + return 1; +} + + +// A singleSampledCurve must survive a round trip as one, rather than being promoted to +// a segmentedCurve, and must still evaluate the same afterwards. +static +cmsInt32Number CheckSingleSampledCurveRoundTrip(void) +{ + static const cmsUInt8Number RampF32[] = { + 0x00, 0x00, 0x00, 0x00, + 0x3E, 0x80, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0x00, + 0x3F, 0x40, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x00 + }; + cmsHPROFILE h = NULL, h2 = NULL; + cmsPipeline* pipe; + cmsPipeline* Dup = NULL; + _cmsStageToneCurvesData* CurveData; + cmsUInt32Number clen = 0; + char* data = NULL; + cmsUInt32Number rc = 0; + cmsFloat32Number vin, vout; + + // A five point ramp over [0.25, 0.75] that extrapolates, so both the sampled + // middle and the outer extension segments have something to preserve + { + cmsUInt8Number buf[256]; + cmsUInt32Number pos = 0; + cmsUInt32Number ElemSize = 12 + 8 + 24 + sizeof(RampF32); + union { cmsFloat32Number f; cmsUInt32Number u; } conv; + + memset(buf, 0, sizeof(buf)); + + buf[pos++] = 'm'; buf[pos++] = 'p'; buf[pos++] = 'e'; buf[pos++] = 't'; + pos += 4; + buf[pos++] = 0; buf[pos++] = 1; + buf[pos++] = 0; buf[pos++] = 1; + buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 1; + buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 24; // element offset + buf[pos++] = (cmsUInt8Number)(ElemSize >> 24); buf[pos++] = (cmsUInt8Number)(ElemSize >> 16); + buf[pos++] = (cmsUInt8Number)(ElemSize >> 8); buf[pos++] = (cmsUInt8Number)(ElemSize); + + buf[pos++] = 'c'; buf[pos++] = 'v'; buf[pos++] = 's'; buf[pos++] = 't'; + pos += 4; + buf[pos++] = 0; buf[pos++] = 1; + buf[pos++] = 0; buf[pos++] = 1; + buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 20; // curve offset + buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; + buf[pos++] = (cmsUInt8Number)(24 + sizeof(RampF32)); // curve size + + buf[pos++] = 's'; buf[pos++] = 'n'; buf[pos++] = 'g'; buf[pos++] = 'f'; + pos += 4; + buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 5; // N + conv.f = 0.25f; + buf[pos++] = (cmsUInt8Number)(conv.u >> 24); buf[pos++] = (cmsUInt8Number)(conv.u >> 16); + buf[pos++] = (cmsUInt8Number)(conv.u >> 8); buf[pos++] = (cmsUInt8Number)(conv.u); + conv.f = 0.75f; + buf[pos++] = (cmsUInt8Number)(conv.u >> 24); buf[pos++] = (cmsUInt8Number)(conv.u >> 16); + buf[pos++] = (cmsUInt8Number)(conv.u >> 8); buf[pos++] = (cmsUInt8Number)(conv.u); + buf[pos++] = 0; buf[pos++] = 1; // extension: extrapolate + buf[pos++] = 0; buf[pos++] = 0; // encoding: float32 + memmove(buf + pos, RampF32, sizeof(RampF32)); + pos += sizeof(RampF32); + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); return 0; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigGrayData); + cmsSetPCS(h, cmsSigLabData); + + if (!cmsWriteRawTag(h, cmsSigDToB3Tag, buf, pos)) { Fail("cmsWriteRawTag failed"); goto Error; } + + cmsSaveProfileToMem(h, NULL, &clen); + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); goto Error; } + cmsSaveProfileToMem(h, data, &clen); + cmsCloseProfile(h); h = NULL; + + h2 = cmsOpenProfileFromMem(data, clen); + free(data); data = NULL; + if (h2 == NULL) { Fail("Cannot reopen the profile"); goto Error; } + } + + pipe = (cmsPipeline*) cmsReadTag(h2, cmsSigDToB3Tag); + if (pipe == NULL) { Fail("Cannot read the fixture DToB3"); goto Error; } + + CurveData = (_cmsStageToneCurvesData*) cmsPipelineGetPtrToFirstStage(pipe) ->Data; + if (CurveData ->TheCurves[0] ->CurveType != cmsSigSingleSampledCurve) { + Fail("The curve was not tagged as a singleSampledCurve on read"); + goto Error; + } + + // cmsDupToneCurve must carry the encoding across, which cmsPipelineDup relies on + Dup = cmsPipelineDup(pipe); + if (Dup == NULL) { Fail("cmsPipelineDup failed"); goto Error; } + + CurveData = (_cmsStageToneCurvesData*) cmsPipelineGetPtrToFirstStage(Dup) ->Data; + if (CurveData ->TheCurves[0] ->CurveType != cmsSigSingleSampledCurve) { + Fail("cmsDupToneCurve lost the singleSampledCurve encoding"); + goto Error; + } + + // Write the duplicate back out and read it once more. The curve must still be an + // 'sngf' and must still extrapolate the same way. + { + cmsHPROFILE h3 = cmsCreateProfilePlaceholder(DbgThread()); + cmsHPROFILE h4; + cmsPipeline* ReRead; + + if (h3 == NULL) { Fail("cmsCreateProfilePlaceholder failed"); goto Error; } + + cmsSetProfileVersion(h3, 5.0); + cmsSetDeviceClass(h3, cmsSigOutputClass); + cmsSetColorSpace(h3, cmsSigGrayData); + cmsSetPCS(h3, cmsSigLabData); + + if (!cmsWriteTag(h3, cmsSigDToB3Tag, Dup)) { Fail("cmsWriteTag failed"); cmsCloseProfile(h3); goto Error; } + + clen = 0; + cmsSaveProfileToMem(h3, NULL, &clen); + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); cmsCloseProfile(h3); goto Error; } + cmsSaveProfileToMem(h3, data, &clen); + cmsCloseProfile(h3); + + h4 = cmsOpenProfileFromMem(data, clen); + free(data); data = NULL; + if (h4 == NULL) { Fail("Cannot reopen the rewritten profile"); goto Error; } + + ReRead = (cmsPipeline*) cmsReadTag(h4, cmsSigDToB3Tag); + if (ReRead == NULL) { Fail("Cannot read the rewritten DToB3"); cmsCloseProfile(h4); goto Error; } + + CurveData = (_cmsStageToneCurvesData*) cmsPipelineGetPtrToFirstStage(ReRead) ->Data; + if (CurveData ->TheCurves[0] ->CurveType != cmsSigSingleSampledCurve) { + Fail("The curve was promoted away from a singleSampledCurve on write"); + cmsCloseProfile(h4); + goto Error; + } + + // Inside the domain, and extrapolated below it + vin = 0.5f; cmsPipelineEvalFloat(&vin, &vout, ReRead); + if (fabs(vout - 0.5f) > 1e-5) { + Fail("After the round trip, 0.5 gave %f", vout); + cmsCloseProfile(h4); + goto Error; + } + + vin = 0.0f; cmsPipelineEvalFloat(&vin, &vout, ReRead); + if (fabs(vout + 0.5f) > 1e-5) { + Fail("After the round trip, extrapolation at 0.0 gave %f, expected -0.5", vout); + cmsCloseProfile(h4); + goto Error; + } + + cmsCloseProfile(h4); + } + + rc = 1; + +Error: + if (Dup != NULL) cmsPipelineFree(Dup); + if (h != NULL) cmsCloseProfile(h); + if (h2 != NULL) cmsCloseProfile(h2); + if (data != NULL) free(data); + return rc; +} + + +// Reads a hybrid printer profile: an ICC.1 CMYK profile carrying an embedded ICC.2 +// sub-profile in its 'ICC5' tag, whose DToB3 tag maps CMYK to a spectral reflectance +// vector. This is the only committed profile that exercises the iccMAX paths, so it +// is mandatory rather than skipped when missing. +// +// HybridPrinterCMYK_small.icc derives from the ICC HybridPrinterWithReflectance ICS +// package, rebuilt with the iccDEV tools, with every CLUT subsampled onto a coarser grid to +// get the file down to a committable size: 4-input CLUTs to 5 points per axis and +// 3-input to 9. Those strides divide the originals exactly, so every value in it is an +// original node value rather than a resampled one. It is therefore structurally +// faithful but far too coarse to be colorimetrically useful, and the ICC.1 and +// embedded ICC.2 renderings no longer track each other. See +// testbed/subsample_clut.py to regenerate it. +// +// The embedded profile declares version 5.1 and a spectral PCS of 'rs' with 36 +// channels, and its DToB3 is 4 to 36 channels built from a curve set, then an +// extendedCLUT reducing to basis coefficients, then a matrix expanding those to the +// spectrum. Three of the four curves are singleSampledCurves; the first was replaced with +// a segmentedCurve holding one ICC.2 formulaCurveSegment of function type 0003h, with +// identity parameters so it changes no output, purely so the fixture exercises the +// formula segment reader as well. Version 5.1 rather than 5.0 is deliberate too: it is +// the case that proves the header version check tolerates a minor bump. +// +// So the fixture covers the sngf curves, an ICC.2-only formula segment, the +// extendedCLUT reader in uInt16 encoding, the wide matrix, the spectral header fields +// and the embedded profile tag all at once. The assertions below pin the element chain +// and the channel relationships rather than literal counts, so a regenerated profile +// with different internal widths still passes. +static +cmsInt32Number CheckHybridPrinterProfile(void) +{ + cmsHPROFILE hBase = NULL; + cmsHPROFILE hSub = NULL; + cmsPipeline* pipe; + const cmsICCData* Embedded; + cmsUInt16Number nSpectralChans; + cmsFloat32Number Start, End; + cmsUInt16Number Steps; + cmsFloat32Number In[4], Out[MAX_STAGE_CHANNELS]; + cmsUInt32Number i; + cmsUInt32Number rc = 0; + cmsBool AnyNonZero; + + // Trap the error instead of letting the global handler end the process. A missing + // path makes cmsOpenProfileFromFile signal cmsERROR_FILE, and FatalErrorQuit + // calls exit(1), which would abort the whole suite rather than failing one test. + cmsSetLogErrorHandler(ErrorReportingFunction); + hBase = cmsOpenProfileFromFile("HybridPrinterCMYK_small.icc", "r"); + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + + if (hBase == NULL) { + + // Committed to the repository, so absence is a broken checkout rather than + // a reason to skip + Fail("Cannot open HybridPrinterCMYK_small.icc"); + return 0; + } + + if (cmsGetColorSpace(hBase) != cmsSigCmykData) { + Fail("The hybrid profile is not CMYK"); + goto Error; + } + + // The containing ICC.1 profile must not itself claim a spectral PCS + if (cmsGetSpectralPCS(hBase) != 0) { + Fail("The containing ICC.1 profile reports a spectral PCS"); + goto Error; + } + + Embedded = (const cmsICCData*) cmsReadTag(hBase, cmsSigEmbeddedV5ProfileTag); + if (Embedded == NULL) { Fail("No ICC5 tag in the hybrid profile"); goto Error; } + + if (Embedded ->len < 132) { Fail("The ICC5 tag is too small: %d", Embedded ->len); goto Error; } + + hSub = cmsOpenProfileFromMem(Embedded ->data, Embedded ->len); + if (hSub == NULL) { Fail("Cannot open the embedded ICC.2 profile"); goto Error; } + + // The embedding technical note requires the sub-profile to be a logical + // replacement: same class, same device space + if (cmsGetDeviceClass(hSub) != cmsGetDeviceClass(hBase)) { + Fail("The embedded profile has a different device class"); + goto Error; + } + + if (cmsGetColorSpace(hSub) != cmsGetColorSpace(hBase)) { + Fail("The embedded profile has a different colour space"); + goto Error; + } + + nSpectralChans = cmsGetSpectralPCSChannels(hSub); + if (nSpectralChans == 0) { Fail("No spectral PCS in the embedded profile"); goto Error; } + + // The channel count implied by the signature must agree with the range steps + if (!cmsGetSpectralPCSRange(hSub, &Start, &End, &Steps)) { + Fail("cmsGetSpectralPCSRange failed on the embedded profile"); + goto Error; + } + + if (Steps != nSpectralChans) { + Fail("Spectral PCS says %d channels but the range says %d steps", nSpectralChans, Steps); + goto Error; + } + + if (!(Start > 200.0f && Start < End && End < 1200.0f)) { + Fail("Implausible spectral range: %f to %f nm", Start, End); + goto Error; + } + + pipe = (cmsPipeline*) cmsReadTag(hSub, cmsSigDToB3Tag); + if (pipe == NULL) { Fail("Cannot read DToB3 from the embedded profile"); goto Error; } + + if (cmsPipelineInputChannels(pipe) != 4) { + Fail("DToB3 has %d input channels, expected 4", cmsPipelineInputChannels(pipe)); + goto Error; + } + + if (cmsPipelineOutputChannels(pipe) != nSpectralChans) { + Fail("DToB3 outputs %d channels but the spectral PCS says %d", + cmsPipelineOutputChannels(pipe), nSpectralChans); + goto Error; + } + + // Walk the elements. The profile is built as a curve set, then an extendedCLUT + // reducing to basis coefficients, then a matrix expanding those to the spectrum. + { + cmsStage* stage = cmsPipelineGetPtrToFirstStage(pipe); + _cmsStageToneCurvesData* CurveData; + cmsUInt32Number c; + + if (cmsPipelineStageCount(pipe) != 3) { + Fail("DToB3 has %d stages, expected 3", cmsPipelineStageCount(pipe)); + goto Error; + } + + if (cmsStageType(stage) != cmsSigCurveSetElemType) { + Fail("The first DToB3 stage is not a curve set"); + goto Error; + } + + // Curve 0 is deliberately a segmentedCurve carrying one ICC.2 formulaCurveSegment of + // function type 0003h -- lcms parametric type 9, Y = a*(b*X + c)^gamma + d -- with + // identity parameters, so the fixture exercises the formula segment reader as well as + // the sampled one. The rest are singleSampledCurves and must be recognised as such + // rather than silently promoted to segmentedCurves. + CurveData = (_cmsStageToneCurvesData*) stage ->Data; + + if (CurveData ->TheCurves[0] ->CurveType == cmsSigSingleSampledCurve) { + Fail("DToB3 curve 0 should be a segmentedCurve, not a singleSampledCurve"); + goto Error; + } + + if (CurveData ->TheCurves[0] ->nSegments != 1 || + CurveData ->TheCurves[0] ->Segments == NULL) { + Fail("DToB3 curve 0 has %d segments, expected 1", + CurveData ->TheCurves[0] ->nSegments); + goto Error; + } + + if (CurveData ->TheCurves[0] ->Segments[0].Type != 9) { + Fail("DToB3 curve 0 segment is parametric type %d, expected 9 (ICC.2 0003h)", + CurveData ->TheCurves[0] ->Segments[0].Type); + goto Error; + } + + // gamma, a, b, c, d + { + static const cmsFloat64Number Identity[5] = { 1.0, 1.0, 1.0, 0.0, 0.0 }; + cmsUInt32Number p; + + for (p = 0; p < 5; p++) { + + if (fabs(CurveData ->TheCurves[0] ->Segments[0].Params[p] - Identity[p]) > 1E-6) { + Fail("DToB3 curve 0 parameter %d is %f, expected %f", p, + CurveData ->TheCurves[0] ->Segments[0].Params[p], Identity[p]); + goto Error; + } + } + } + + // Identity parameters, so the curve must be a no-op. This also proves the evaluator is + // wired to the type the reader assigned. + if (fabs(cmsEvalToneCurveFloat(CurveData ->TheCurves[0], 0.25f) - 0.25) > 1E-6 || + fabs(cmsEvalToneCurveFloat(CurveData ->TheCurves[0], 1.00f) - 1.00) > 1E-6) { + Fail("DToB3 curve 0 does not evaluate as an identity"); + goto Error; + } + + for (c = 1; c < cmsStageInputChannels(stage); c++) { + + if (CurveData ->TheCurves[c] ->CurveType != cmsSigSingleSampledCurve) { + Fail("DToB3 curve %d is not marked as a singleSampledCurve", c); + goto Error; + } + } + + stage = cmsStageNext(stage); + if (cmsStageType(stage) != (cmsStageSignature) cmsSigExtCLutElemType) { + Fail("The second DToB3 stage is not an extendedCLUT"); + goto Error; + } + + stage = cmsStageNext(stage); + if (cmsStageType(stage) != cmsSigMatrixElemType) { + Fail("The third DToB3 stage is not a matrix"); + goto Error; + } + + if (cmsStageInputChannels(stage) <= 4 || cmsStageOutputChannels(stage) != nSpectralChans) { + Fail("The expanding matrix is %d to %d, expected something to %d", + cmsStageInputChannels(stage), cmsStageOutputChannels(stage), nSpectralChans); + goto Error; + } + } + + // Paper white: no ink at all should give a high, non-zero reflectance everywhere + In[0] = 0.0f; In[1] = 0.0f; In[2] = 0.0f; In[3] = 0.0f; + memset(Out, 0, sizeof(Out)); + cmsPipelineEvalFloat(In, Out, pipe); + + AnyNonZero = FALSE; + for (i = 0; i < nSpectralChans; i++) { + + if (Out[i] != 0.0f) AnyNonZero = TRUE; + + // Reflectance is relative to the perfect reflector, so stay in a sane band + if (Out[i] < -0.01f || Out[i] > 2.0f) { + Fail("Reflectance[%d] of paper white is %f, out of range", i, Out[i]); + goto Error; + } + } + + if (!AnyNonZero) { Fail("DToB3 gave an all zero spectrum for paper white"); goto Error; } + + // Solid ink must come back darker than bare paper, summed across the spectrum + { + cmsFloat32Number WhiteSum = 0.0f, InkSum = 0.0f; + + for (i = 0; i < nSpectralChans; i++) WhiteSum += Out[i]; + + In[0] = 1.0f; In[1] = 1.0f; In[2] = 1.0f; In[3] = 1.0f; + cmsPipelineEvalFloat(In, Out, pipe); + + for (i = 0; i < nSpectralChans; i++) InkSum += Out[i]; + + if (!(InkSum < WhiteSum)) { + Fail("Full ink reflects as much as paper white: %f vs %f", InkSum, WhiteSum); + goto Error; + } + } + + rc = 1; + +Error: + if (hSub != NULL) cmsCloseProfile(hSub); + if (hBase != NULL) cmsCloseProfile(hBase); + return rc; +} + +// swpt round-trip through the idiomatic cmsReadTag/cmsWriteTag path. Task 3 extends +// this function with the raw-tag accessor path, including ui16. +static +cmsInt32Number CheckSpectralWhitePointRoundTrip(void) +{ + cmsHPROFILE h; + cmsFloatArray* w; + cmsFloatArray* r; + cmsUInt8Number* Mem = NULL; + cmsUInt32Number Size = 0; + cmsUInt32Number i; + cmsInt32Number rc = 0; + cmsFloat32Number Fl16Values[36]; // values read back from the fl16 tag, kept for + // an exact comparison against the fl32 promotion + + // Write 36 values as fl32, reload, and compare exactly: float32 is lossless + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) return 0; + + cmsSetProfileVersion(h, 5.0); + + w = cmsAllocFloatArray(DbgThread(), 36); + if (w == NULL) { cmsCloseProfile(h); return 0; } + + for (i = 0; i < 36; i++) + w ->Values[i] = (cmsFloat32Number) (0.25 + i * 0.02); + + if (!cmsWriteTag(h, cmsSigSpectralWhitePointTag, w)) { + + Fail("Could not write swpt as fl32"); + goto Cleanup; + } + + if (!cmsSaveProfileToMem(h, NULL, &Size) || Size == 0) { + + Fail("Could not size swpt profile"); + goto Cleanup; + } + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(h, Mem, &Size)) { + + Fail("Could not save swpt profile"); + goto Cleanup; + } + + cmsCloseProfile(h); + h = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h == NULL) { + + Fail("Could not reopen swpt profile"); + goto Cleanup; + } + + r = (cmsFloatArray*) cmsReadTag(h, cmsSigSpectralWhitePointTag); + if (r == NULL) { + + Fail("Could not read swpt back"); + goto Cleanup; + } + + if (r ->nValues != 36) { + + Fail("swpt came back with %d values, expected 36", r ->nValues); + goto Cleanup; + } + + for (i = 0; i < 36; i++) { + + if (r ->Values[i] != w ->Values[i]) { + + Fail("swpt fl32 value %d changed: got %f expected %f", + i, r ->Values[i], w ->Values[i]); + goto Cleanup; + } + } + + // Now the same array authored as fl16 through the raw-tag accessor -- + // cmsWriteSpectralWhitePoint still emits any of the three encodings ICC.2 + // permits, which is what lets this test put a real fl16 tag on disk to read + // back through the registered handler below. + if (!cmsWriteSpectralWhitePoint(h, w, cmsSigFloat16ArrayType)) { + + Fail("Could not write swpt as fl16"); + goto Cleanup; + } + + free(Mem); + Mem = NULL; + Size = 0; + + if (!cmsSaveProfileToMem(h, NULL, &Size) || Size == 0) goto Cleanup; + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(h, Mem, &Size)) goto Cleanup; + + cmsCloseProfile(h); + h = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h == NULL) goto Cleanup; + + // Confirm the on-disk type signature really is 'fl16' (0x666C3136), independent + // of any registered type handler: bytes 0..3 of the raw tag are the type + // signature, big-endian on the wire. + { + cmsUInt32Number RawSig; + + if (cmsReadRawTag(h, cmsSigSpectralWhitePointTag, &RawSig, sizeof(RawSig)) != sizeof(RawSig)) { + + Fail("Could not read swpt raw tag header"); + goto Cleanup; + } + + if (_cmsAdjustEndianess32(RawSig) != cmsSigFloat16ArrayType) { + + Fail("swpt written as fl16 was not saved with the fl16 type signature"); + goto Cleanup; + } + } + + // Read it back through cmsReadTag -- the registered fl16 handler. This is the + // regression this change most risks: SupportedTypes still lists fl16 so + // cmsReadTag can still decode it, even though DecideType no longer exists to + // pick fl16 back up on write. + r = (cmsFloatArray*) cmsReadTag(h, cmsSigSpectralWhitePointTag); + if (r == NULL) { + + Fail("Could not read fl16 swpt back through cmsReadTag"); + goto Cleanup; + } + + if (r ->nValues != 36) { + + Fail("fl16 swpt came back with %d values, expected 36", r ->nValues); + goto Cleanup; + } + + for (i = 0; i < 36; i++) { + + if (fabs(r ->Values[i] - w ->Values[i]) > 1E-3) { + + Fail("swpt fl16 value %d changed: got %f expected %f", + i, r ->Values[i], w ->Values[i]); + goto Cleanup; + } + } + + memcpy(Fl16Values, r ->Values, sizeof(Fl16Values)); + + // Write that same object (r, just read back as fl16) through cmsWriteTag. The + // target is a fresh profile rather than h itself: cmsWriteTag's _cmsNewTag frees + // whatever the destination slot already holds before duplicating "data" into it, + // so writing into the very slot "r" was read from would read data through a + // pointer already freed by that same call. A second profile sidesteps that + // without changing what is being proven: with DecideType gone, SupportedTypes[0] + // (fl32) is what cmsWriteTag picks, pinning the policy that a tag read as fl16 + // is always written back as fl32. + { + cmsHPROFILE hProm = cmsCreateProfilePlaceholder(DbgThread()); + cmsUInt32Number RawSig; + cmsFloatArray* rp; + + if (hProm == NULL) goto Cleanup; + + cmsSetProfileVersion(hProm, 5.0); + + if (!cmsWriteTag(hProm, cmsSigSpectralWhitePointTag, r)) { + + Fail("Could not write fl16-read swpt back"); + cmsCloseProfile(hProm); + goto Cleanup; + } + + free(Mem); + Mem = NULL; + Size = 0; + + if (!cmsSaveProfileToMem(hProm, NULL, &Size) || Size == 0) { cmsCloseProfile(hProm); goto Cleanup; } + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) { cmsCloseProfile(hProm); goto Cleanup; } + + if (!cmsSaveProfileToMem(hProm, Mem, &Size)) { cmsCloseProfile(hProm); goto Cleanup; } + + cmsCloseProfile(hProm); + + hProm = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (hProm == NULL) goto Cleanup; + + // Pins the documented policy: read fl16, write fl32 (0x666C3332). + if (cmsReadRawTag(hProm, cmsSigSpectralWhitePointTag, &RawSig, sizeof(RawSig)) != sizeof(RawSig)) { + + Fail("Could not read promoted swpt raw tag header"); + cmsCloseProfile(hProm); + goto Cleanup; + } + + if (_cmsAdjustEndianess32(RawSig) != cmsSigFloat32ArrayType) { + + Fail("swpt read as fl16 was not promoted to fl32 on write"); + cmsCloseProfile(hProm); + goto Cleanup; + } + + rp = (cmsFloatArray*) cmsReadTag(hProm, cmsSigSpectralWhitePointTag); + if (rp == NULL) { + + Fail("Could not read fl32-promoted swpt back"); + cmsCloseProfile(hProm); + goto Cleanup; + } + + if (rp ->nValues != 36) { + + Fail("fl32-promoted swpt came back with %d values, expected 36", rp ->nValues); + cmsCloseProfile(hProm); + goto Cleanup; + } + + // The values going in were already half-precision (read back from fl16 + // above), and fl32 is lossless from fl16, so the promotion must reproduce + // them exactly -- an exact comparison is correct here, not a tolerance. + for (i = 0; i < 36; i++) { + + if (rp ->Values[i] != Fl16Values[i]) { + + Fail("swpt fl32-promoted value %d changed: got %f expected %f", + i, rp ->Values[i], Fl16Values[i]); + cmsCloseProfile(hProm); + goto Cleanup; + } + } + + cmsCloseProfile(hProm); + } + + // Now the accessor path, which also covers ui16. Each encoding is written, saved, + // reloaded and read back through cmsReadSpectralWhitePoint. + { + static const cmsTagTypeSignature Encodings[3] = { + cmsSigFloat32ArrayType, cmsSigFloat16ArrayType, cmsSigUInt16ArrayType }; + static const cmsFloat64Number Tolerances[3] = { 1E-6, 1E-3, 2E-5 }; + cmsUInt32Number e; + + for (e = 0; e < 3; e++) { + + cmsFloatArray* got = NULL; + cmsHPROFILE h2 = cmsCreateProfilePlaceholder(DbgThread()); + + if (h2 == NULL) goto Cleanup; + + cmsSetProfileVersion(h2, 5.0); + + if (!cmsWriteSpectralWhitePoint(h2, w, Encodings[e])) { + + Fail("cmsWriteSpectralWhitePoint refused encoding %d", e); + cmsCloseProfile(h2); + goto Cleanup; + } + + free(Mem); + Mem = NULL; + Size = 0; + + if (!cmsSaveProfileToMem(h2, NULL, &Size) || Size == 0) { cmsCloseProfile(h2); goto Cleanup; } + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) { cmsCloseProfile(h2); goto Cleanup; } + + if (!cmsSaveProfileToMem(h2, Mem, &Size)) { cmsCloseProfile(h2); goto Cleanup; } + + cmsCloseProfile(h2); + + h2 = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h2 == NULL) goto Cleanup; + + // Directly verifies cmsWriteSpectralWhitePoint emitted the requested + // encoding: bytes 0..3 of the raw tag are the type signature. + { + cmsUInt32Number RawSig; + + if (cmsReadRawTag(h2, cmsSigSpectralWhitePointTag, &RawSig, sizeof(RawSig)) != sizeof(RawSig)) { + + Fail("Could not read raw tag header for encoding %d", e); + cmsCloseProfile(h2); + goto Cleanup; + } + + if (_cmsAdjustEndianess32(RawSig) != (cmsUInt32Number) Encodings[e]) { + + Fail("Accessor round trip %d saved with type signature %x, expected %x", + e, _cmsAdjustEndianess32(RawSig), Encodings[e]); + cmsCloseProfile(h2); + goto Cleanup; + } + } + + if (!cmsReadSpectralWhitePoint(h2, &got) || got == NULL) { + + Fail("cmsReadSpectralWhitePoint failed for encoding %d", e); + cmsCloseProfile(h2); + goto Cleanup; + } + + if (got ->nValues != 36) { + + Fail("Accessor round trip %d returned %d values, expected 36", + e, got ->nValues); + cmsFreeFloatArray(got); + cmsCloseProfile(h2); + goto Cleanup; + } + + for (i = 0; i < 36; i++) { + + if (fabs(got ->Values[i] - w ->Values[i]) > Tolerances[e]) { + + Fail("Accessor round trip %d value %d: got %f expected %f", + e, i, got ->Values[i], w ->Values[i]); + cmsFreeFloatArray(got); + cmsCloseProfile(h2); + goto Cleanup; + } + } + + cmsFreeFloatArray(got); + cmsCloseProfile(h2); + } + + // An encoding ICC.2 does not permit must be refused, not written anyway. + // cmsWriteSpectralWhitePoint signals an error on the way to refusing, so the + // global handler must be swapped out first or FatalErrorQuit would exit(1). + { + cmsHPROFILE h3 = cmsCreateProfilePlaceholder(DbgThread()); + cmsBool WroteBadEncoding; + + if (h3 == NULL) goto Cleanup; + + cmsSetProfileVersion(h3, 5.0); + + cmsSetLogErrorHandler(ErrorReportingFunction); + WroteBadEncoding = cmsWriteSpectralWhitePoint(h3, w, cmsSigUInt8ArrayType); + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + + if (WroteBadEncoding) { + + Fail("cmsWriteSpectralWhitePoint accepted ui08, which ICC.2 forbids for swpt"); + cmsCloseProfile(h3); + goto Cleanup; + } + + cmsCloseProfile(h3); + } + } + + // ui16's write-time clamp order matters: isnan(x) must be checked before the + // x > 1.0 / x < 0.0 relational tests, because every IEEE 754 relational + // comparison involving NaN is false, so a reordering would let NaN pass through + // un-clamped instead of becoming 0. Exercise NaN, +/-infinity, and plain + // out-of-range values against the reference mapping (icFtoU16 in the reference + // implementation): NaN -> 0, +Inf -> 1.0, -Inf -> 0, >1 -> 1.0, <0 -> 0. NaN and + // infinity are produced via sqrt(-1.0)/log(0.0) rather than a literal division + // by zero, so as not to trip a constant-division-by-zero warning. + { + cmsFloatArray* special = cmsAllocFloatArray(DbgThread(), 5); + cmsFloatArray* gotSpecial = NULL; + cmsHPROFILE h4; + static const cmsFloat64Number Expected[5] = { 0.0, 1.0, 0.0, 1.0, 0.0 }; + + if (special == NULL) goto Cleanup; + + special ->Values[0] = (cmsFloat32Number) sqrt(-1.0); // NaN + special ->Values[1] = (cmsFloat32Number) (-log(0.0)); // +Infinity + special ->Values[2] = (cmsFloat32Number) log(0.0); // -Infinity + special ->Values[3] = 5.0f; // above 1.0 + special ->Values[4] = -5.0f; // below 0.0 + + h4 = cmsCreateProfilePlaceholder(DbgThread()); + if (h4 == NULL) { cmsFreeFloatArray(special); goto Cleanup; } + + cmsSetProfileVersion(h4, 5.0); + + if (!cmsWriteSpectralWhitePoint(h4, special, cmsSigUInt16ArrayType)) { + + Fail("cmsWriteSpectralWhitePoint refused NaN/infinity values as ui16"); + cmsFreeFloatArray(special); + cmsCloseProfile(h4); + goto Cleanup; + } + + cmsFreeFloatArray(special); + + free(Mem); + Mem = NULL; + Size = 0; + + if (!cmsSaveProfileToMem(h4, NULL, &Size) || Size == 0) { cmsCloseProfile(h4); goto Cleanup; } + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) { cmsCloseProfile(h4); goto Cleanup; } + + if (!cmsSaveProfileToMem(h4, Mem, &Size)) { cmsCloseProfile(h4); goto Cleanup; } + + cmsCloseProfile(h4); + + h4 = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h4 == NULL) goto Cleanup; + + if (!cmsReadSpectralWhitePoint(h4, &gotSpecial) || gotSpecial == NULL || gotSpecial ->nValues != 5) { + + Fail("Could not read back the NaN/infinity ui16 probe"); + if (gotSpecial != NULL) cmsFreeFloatArray(gotSpecial); + cmsCloseProfile(h4); + goto Cleanup; + } + + for (i = 0; i < 5; i++) { + + if (fabs(gotSpecial ->Values[i] - Expected[i]) > 2E-5) { + + Fail("ui16 special value %d: got %f, expected %f", + i, gotSpecial ->Values[i], Expected[i]); + cmsFreeFloatArray(gotSpecial); + cmsCloseProfile(h4); + goto Cleanup; + } + } + + cmsFreeFloatArray(gotSpecial); + cmsCloseProfile(h4); + } + + rc = 1; + +Cleanup: + if (Mem != NULL) free(Mem); + cmsFreeFloatArray(w); + if (h != NULL) cmsCloseProfile(h); + + return rc; +} + +// N and M are deliberately different, and the three observer vectors deliberately +// hold different values, so a transposed X/Y/Z ordering or a swapped N/M cannot pass. +static +cmsInt32Number CheckSpectralViewingConditionsRoundTrip(void) +{ + cmsHPROFILE h = NULL; + cmsSpectralViewingConditions* w = NULL; + cmsSpectralViewingConditions* r; + cmsUInt8Number* Mem = NULL; + cmsUInt32Number Size = 0, TagSize; + cmsUInt32Number i; + cmsInt32Number rc = 0; + const cmsUInt16Number N = 5, M = 7; + + // Regression-test state for the hand-corrupted N == 0x0100 bounds probe below. + // ICC.2 reuses the 'svcn' FourCC for both cmsSigSpectralViewingConditionsTag + // (the tag directory entry) and cmsSigSpectralViewingConditionsType (the type + // header at the start of the tag's own data), so a bare 4-byte search for + // 'svcn' matches the directory entry first -- it precedes the tag data pool + // and is followed by a nonzero file offset, not by the type header's 4 + // reserved zero bytes. Searching for the 8-byte pattern (signature + reserved) + // lands on the type header instead. + static const cmsUInt8Number svcnSig[8] = { 0x73, 0x76, 0x63, 0x6E, 0x00, 0x00, 0x00, 0x00 }; // 'svcn' + reserved + cmsUInt8Number* corrupted = NULL; + cmsHPROFILE hBad = NULL; + cmsSpectralViewingConditions* rBad = NULL; + cmsUInt32Number sigOffset, k; + + w = cmsAllocSpectralViewingConditions(DbgThread(), N, M); + if (w == NULL) return 0; + + w ->ObserverType = 1; // CIE 1931 + w ->ObserverStart = 400.0f; + w ->ObserverEnd = 500.0f; + w ->IlluminantType = 9; // black body defined by CCT + w ->CCT = 5000.0f; + w ->IlluminantStart = 380.0f; + w ->IlluminantEnd = 440.0f; + + // X vector 1..5, Y vector 11..15, Z vector 21..25 + for (i = 0; i < 3u * N; i++) + w ->Observer[i] = (cmsFloat32Number) (1 + (i / N) * 10 + (i % N)); + + for (i = 0; i < M; i++) + w ->Illuminant[i] = (cmsFloat32Number) (0.5 + i); + + w ->IlluminantXYZ.X = 96.42; w ->IlluminantXYZ.Y = 100.0; w ->IlluminantXYZ.Z = 82.49; + w ->SurroundXYZ.X = 19.28; w ->SurroundXYZ.Y = 20.0; w ->SurroundXYZ.Z = 16.50; + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) goto Cleanup; + + cmsSetProfileVersion(h, 5.0); + + if (!cmsWriteTag(h, cmsSigSpectralViewingConditionsTag, w)) { + + Fail("Could not write svcn"); + goto Cleanup; + } + + if (!cmsSaveProfileToMem(h, NULL, &Size) || Size == 0) goto Cleanup; + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(h, Mem, &Size)) goto Cleanup; + + cmsCloseProfile(h); + h = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h == NULL) { + + Fail("Could not reopen svcn profile"); + goto Cleanup; + } + + // ICC.2 Table 69: the whole tag is 60 + 12N + 4M bytes + TagSize = cmsReadRawTag(h, cmsSigSpectralViewingConditionsTag, NULL, 0); + if (TagSize != 60u + 12u * N + 4u * M) { + + Fail("svcn tag is %d bytes, expected %d", TagSize, 60u + 12u * N + 4u * M); + goto Cleanup; + } + + r = (cmsSpectralViewingConditions*) cmsReadTag(h, cmsSigSpectralViewingConditionsTag); + if (r == NULL) { + + Fail("Could not read svcn back"); + goto Cleanup; + } + + if (r ->ObserverSteps != N || r ->IlluminantSteps != M) { + + Fail("svcn steps came back as N=%d M=%d, expected N=%d M=%d", + r ->ObserverSteps, r ->IlluminantSteps, N, M); + goto Cleanup; + } + + if (r ->ObserverType != w ->ObserverType || r ->IlluminantType != w ->IlluminantType) { + + Fail("svcn observer or illuminant type changed"); + goto Cleanup; + } + + if (r ->ObserverStart != w ->ObserverStart || r ->ObserverEnd != w ->ObserverEnd || + r ->IlluminantStart != w ->IlluminantStart || r ->IlluminantEnd != w ->IlluminantEnd) { + + Fail("svcn spectral range changed"); + goto Cleanup; + } + + if (r ->CCT != w ->CCT) { + + Fail("svcn CCT changed: got %f expected %f", r ->CCT, w ->CCT); + goto Cleanup; + } + + for (i = 0; i < 3u * N; i++) { + + if (r ->Observer[i] != w ->Observer[i]) { + + Fail("svcn observer value %d changed: got %f expected %f", + i, r ->Observer[i], w ->Observer[i]); + goto Cleanup; + } + } + + for (i = 0; i < M; i++) { + + if (r ->Illuminant[i] != w ->Illuminant[i]) { + + Fail("svcn illuminant value %d changed: got %f expected %f", + i, r ->Illuminant[i], w ->Illuminant[i]); + goto Cleanup; + } + } + + // These two triples are stored as float32, not XYZNumber (ICC.2 Table 69 says + // XYZNumber, but that is a spec-table error -- see the comment in + // Type_SpectralViewingConditions_Read). A round trip through cmsCIEXYZ's + // float64 members therefore loses only float64->float32 precision, well + // inside this tolerance. + if (fabs(r ->IlluminantXYZ.X - w ->IlluminantXYZ.X) > 1E-4 || + fabs(r ->IlluminantXYZ.Y - w ->IlluminantXYZ.Y) > 1E-4 || + fabs(r ->IlluminantXYZ.Z - w ->IlluminantXYZ.Z) > 1E-4 || + fabs(r ->SurroundXYZ.X - w ->SurroundXYZ.X) > 1E-4 || + fabs(r ->SurroundXYZ.Y - w ->SurroundXYZ.Y) > 1E-4 || + fabs(r ->SurroundXYZ.Z - w ->SurroundXYZ.Z) > 1E-4) { + + Fail("svcn XYZ triples changed"); + goto Cleanup; + } + + // Regression test for the bounds guard in Type_SpectralViewingConditions_Read: + // a bounds guard has already regressed once on this branch, so hand-corrupt + // the already-serialized tag's N field to claim 256 observer steps -- 0x0100, + // inside a tag whose actual on-disk size cannot possibly hold 256*3 float32 + // values -- and confirm the reader rejects it rather than over-reading. + sigOffset = (cmsUInt32Number) -1; + for (k = 0; k + 8 <= Size; k++) { + if (memcmp(Mem + k, svcnSig, 8) == 0) { sigOffset = k; break; } + } + + if (sigOffset == (cmsUInt32Number) -1) { + Fail("Could not locate the svcn type header in the serialized profile"); + goto Cleanup; + } + + corrupted = (cmsUInt8Number*) malloc(Size); + if (corrupted == NULL) { Fail("malloc failed"); goto Cleanup; } + memcpy(corrupted, Mem, Size); + + // Signature (4) + reserved (4) + observerType (4) + ObserverStart (2) + + // ObserverEnd (2) puts N's big-endian uInt16 at offset +16 from the signature. + corrupted[sigOffset + 16] = 0x01; + corrupted[sigOffset + 17] = 0x00; // N = 256 + + // Rejection goes through cmsSignalError(cmsERROR_CORRUPTION_DETECTED, ...) + // (src/cmsio0.c, the tag-read framework's "Corrupted tag" path), which the + // installed FatalErrorQuit handler treats as fatal, so it is swapped out for + // the non-fatal ErrorReportingFunction for the duration of this probe, exactly + // as other negative-case tests in this file do (e.g. CheckICCMAXFormulaRoundTrip). + // No goto/return/Fail between the swap and the restore, so the handler cannot + // be left non-fatal on any exit path from this window. + cmsSetLogErrorHandler(ErrorReportingFunction); + TrappedError = FALSE; + SimultaneousErrors = 0; + + hBad = cmsOpenProfileFromMemTHR(DbgThread(), corrupted, Size); + if (hBad != NULL) { + rBad = (cmsSpectralViewingConditions*) cmsReadTag(hBad, cmsSigSpectralViewingConditionsTag); + } + + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + + if (rBad != NULL) { + + Fail("svcn declaring N = 0x0100 inside an undersized tag was not rejected"); + goto Cleanup; + } + + rc = 1; + +Cleanup: + if (corrupted != NULL) free(corrupted); + if (hBad != NULL) cmsCloseProfile(hBad); + if (Mem != NULL) free(Mem); + cmsFreeSpectralViewingConditions(w); + if (h != NULL) cmsCloseProfile(h); + + return rc; +} + +// Reads the committed hybrid printer fixture and checks both spectral tags against +// values produced by the reference implementation, not by our own writer. +static +cmsInt32Number CheckSpectralTagsAgainstFixture(void) +{ + cmsHPROFILE outer = NULL, sub = NULL; + cmsICCData* Embedded; + cmsFloatArray* w; + cmsSpectralViewingConditions* sv; + cmsInt32Number rc = 0; + + outer = cmsOpenProfileFromFileTHR(DbgThread(), "HybridPrinterCMYK_small.icc", "r"); + if (outer == NULL) { + + Fail("Could not open HybridPrinterCMYK_small.icc"); + return 0; + } + + Embedded = (cmsICCData*) cmsReadTag(outer, cmsSigEmbeddedV5ProfileTag); + if (Embedded == NULL) { + + Fail("Fixture has no ICC5 tag"); + goto Cleanup; + } + + sub = cmsOpenProfileFromMemTHR(DbgThread(), Embedded ->data, Embedded ->len); + if (sub == NULL) { + + Fail("Could not open the embedded ICC.2 profile"); + goto Cleanup; + } + + w = (cmsFloatArray*) cmsReadTag(sub, cmsSigSpectralWhitePointTag); + if (w == NULL) { + + Fail("Fixture sub-profile has no readable swpt"); + goto Cleanup; + } + + if (w ->nValues != 36) { + + Fail("Fixture swpt has %d values, expected 36", w ->nValues); + goto Cleanup; + } + + if (fabs(w ->Values[0] - 0.27557) > 1E-4) { + + Fail("Fixture swpt[0] is %f, expected 0.27557", w ->Values[0]); + goto Cleanup; + } + + sv = (cmsSpectralViewingConditions*) cmsReadTag(sub, cmsSigSpectralViewingConditionsTag); + if (sv == NULL) { + + Fail("Fixture sub-profile has no readable svcn"); + goto Cleanup; + } + + if (sv ->ObserverType != 1) { + + Fail("Fixture observer type is %d, expected 1 (CIE 1931)", sv ->ObserverType); + goto Cleanup; + } + + if (sv ->ObserverSteps != 81 || sv ->IlluminantSteps != 81) { + + Fail("Fixture svcn steps are N=%d M=%d, expected 81 and 81", + sv ->ObserverSteps, sv ->IlluminantSteps); + goto Cleanup; + } + + // 380 and 780 are exactly representable in float16, so this compares exactly + // (to floating point noise). + if (fabs(sv ->ObserverStart - 380.0) > 1E-5 || fabs(sv ->ObserverEnd - 780.0) > 1E-5) { + + Fail("Fixture observer range is %f..%f, expected 380..780", + sv ->ObserverStart, sv ->ObserverEnd); + goto Cleanup; + } + + // The first CMF triple is the textbook CIE 1931 tristimulus at 380 nm, which is + // also what proves the matrix is stored as X-vector then Y then Z rather than + // interleaved: these three come from the heads of three separate vectors. + if (fabs(sv ->Observer[0] - 0.001370) > 1E-5 || + fabs(sv ->Observer[81] - 0.000040) > 1E-5 || + fabs(sv ->Observer[162] - 0.006450) > 1E-5) { + + Fail("Fixture first CMF triple is %f/%f/%f, expected 0.001370/0.000040/0.006450", + sv ->Observer[0], sv ->Observer[81], sv ->Observer[162]); + goto Cleanup; + } + + if (sv ->IlluminantType != 1) { + + Fail("Fixture illuminant type is %d, expected 1 (D50)", sv ->IlluminantType); + goto Cleanup; + } + + // 5000 is exact in float32, so this compares exactly (to floating point noise). + if (fabs(sv ->CCT - 5000.0) > 1E-5) { + + Fail("Fixture CCT is %f, expected 5000", sv ->CCT); + goto Cleanup; + } + + if (fabs(sv ->Illuminant[0] - 24.457) > 1E-2) { + + Fail("Fixture illuminant[0] is %f, expected about 24.457", sv ->Illuminant[0]); + goto Cleanup; + } + + // ICC.2 Table 69 says XYZNumber (s15Fixed16) for these two triples, but the + // reference implementation and this fixture both encode them as float32 -- + // see the matching comment in Type_SpectralViewingConditions_Read. Pinned to + // the fixture's actual bytes (43 1a 47 7f / 43 20 00 00 / 43 03 f2 db as + // float32 = 154.279 / 160.0 / 131.949, i.e. D50 at 160 cd/m2) rather than + // just checking Y > 0: reading these bytes as s15Fixed16 instead would give + // a physically nonsensical near-equal-energy white at Y = 17184, which a + // Y > 0 check alone cannot catch. In this fixture the illuminant and + // surround triples happen to be byte-identical, so this test cannot detect + // an illuminant/surround swap -- only that both decode to the right value. + if (fabs(sv ->IlluminantXYZ.X - 154.279) > 1E-3 || + fabs(sv ->IlluminantXYZ.Y - 160.0) > 1E-3 || + fabs(sv ->IlluminantXYZ.Z - 131.949) > 1E-3 || + fabs(sv ->SurroundXYZ.X - 154.279) > 1E-3 || + fabs(sv ->SurroundXYZ.Y - 160.0) > 1E-3 || + fabs(sv ->SurroundXYZ.Z - 131.949) > 1E-3) { + + Fail("Fixture svcn illuminant/surround XYZ is %f/%f/%f and %f/%f/%f, expected 154.279/160.0/131.949 for both", + sv ->IlluminantXYZ.X, sv ->IlluminantXYZ.Y, sv ->IlluminantXYZ.Z, + sv ->SurroundXYZ.X, sv ->SurroundXYZ.Y, sv ->SurroundXYZ.Z); + goto Cleanup; + } + + rc = 1; + +Cleanup: + if (sub != NULL) cmsCloseProfile(sub); + if (outer != NULL) cmsCloseProfile(outer); + + return rc; +} + +// Identity sampler for a float CLUT: Cargo points at the channel count. +static +cmsInt32Number IdentitySamplerFloatForHybrid(const cmsFloat32Number In[], cmsFloat32Number Out[], void* Cargo) +{ + cmsUInt32Number nChan = *(cmsUInt32Number*) Cargo; + cmsUInt32Number i; + + for (i = 0; i < nChan; i++) + Out[i] = In[i]; + + return 1; +} + +// Builds a hybrid printer profile from nothing and reads every spectral part back. +// If this cannot be written without hand-assembling tags, the feature has not +// achieved its goal. +static +cmsInt32Number CheckAuthorHybridProfile(void) +{ + cmsHPROFILE sub = NULL, outer = NULL, reopened = NULL, extracted = NULL; + cmsSpectralViewingConditions* sv = NULL; + cmsFloatArray* w = NULL; + cmsPipeline* lut = NULL; + cmsICCData* Embedded; + cmsUInt8Number* SubMem = NULL; + cmsUInt8Number* OuterMem = NULL; + cmsUInt32Number SubSize = 0, OuterSize = 0; + cmsUInt32Number i; + cmsUInt32Number ChanCount; + cmsInt32Number rc = 0; + const cmsUInt16Number Channels = 4; + + ChanCount = Channels; + + // ---- the ICC.2 sub-profile ---- + sub = cmsCreateProfilePlaceholder(DbgThread()); + if (sub == NULL) return 0; + + cmsSetProfileVersion(sub, 5.0); + cmsSetDeviceClass(sub, cmsSigOutputClass); + cmsSetColorSpace(sub, cmsSigCmykData); + cmsSetPCS(sub, cmsSigLabData); + + // 'rs' 0004h: reflectance spectra, 4 channels (ICC.2 Table 21) + if (!cmsSetSpectralPCS(sub, 0x72730000u | Channels)) { + + Fail("cmsSetSpectralPCS refused a version 5.0 profile"); + goto Cleanup; + } + + if (!cmsSetSpectralPCSRange(sub, 400.0f, 700.0f, Channels)) { + + Fail("cmsSetSpectralPCSRange failed"); + goto Cleanup; + } + + w = cmsAllocFloatArray(DbgThread(), Channels); + if (w == NULL) goto Cleanup; + + for (i = 0; i < Channels; i++) + w ->Values[i] = (cmsFloat32Number) (0.8 + i * 0.01); + + if (!cmsWriteTag(sub, cmsSigSpectralWhitePointTag, w)) { + + Fail("Could not write swpt into the authored sub-profile"); + goto Cleanup; + } + + sv = cmsAllocSpectralViewingConditions(DbgThread(), 3, 3); + if (sv == NULL) goto Cleanup; + + sv ->ObserverType = 1; + sv ->ObserverStart = 400.0f; + sv ->ObserverEnd = 700.0f; + sv ->IlluminantType = 1; // D50 + sv ->CCT = 5000.0f; + sv ->IlluminantStart = 400.0f; + sv ->IlluminantEnd = 700.0f; + + for (i = 0; i < 9; i++) + sv ->Observer[i] = (cmsFloat32Number) (i + 1); + + for (i = 0; i < 3; i++) + sv ->Illuminant[i] = (cmsFloat32Number) (100 + i); + + sv ->IlluminantXYZ.X = 96.42; sv ->IlluminantXYZ.Y = 100.0; sv ->IlluminantXYZ.Z = 82.49; + sv ->SurroundXYZ.X = 19.28; sv ->SurroundXYZ.Y = 20.0; sv ->SurroundXYZ.Z = 16.50; + + if (!cmsWriteTag(sub, cmsSigSpectralViewingConditionsTag, sv)) { + + Fail("Could not write svcn into the authored sub-profile"); + goto Cleanup; + } + + // A minimal DToB3: 4 device channels in, 4 spectral channels out. The CLUT stage + // must hold float samples: _cmsStageAllocIdentityCLut builds a 16 bit CLUT, and + // the mpet element writer (Type_MPEclut_Write, src/cmstypes.c) refuses anything + // that is not HasFloatValues, so the identity grid is built directly as a float + // CLUT here instead. + lut = cmsPipelineAlloc(DbgThread(), Channels, Channels); + if (lut == NULL) goto Cleanup; + + { + cmsStage* identity = cmsStageAllocCLutFloat(DbgThread(), 2, Channels, Channels, NULL); + + if (identity == NULL || !cmsStageSampleCLutFloat(identity, IdentitySamplerFloatForHybrid, &ChanCount, 0)) { + + if (identity != NULL) cmsStageFree(identity); + Fail("Could not build the authored DToB3 pipeline"); + goto Cleanup; + } + + if (!cmsPipelineInsertStage(lut, cmsAT_BEGIN, identity)) { + + cmsStageFree(identity); + Fail("Could not build the authored DToB3 pipeline"); + goto Cleanup; + } + } + + if (!cmsWriteTag(sub, cmsSigDToB3Tag, lut)) { + + Fail("Could not write DToB3 into the authored sub-profile"); + goto Cleanup; + } + + if (!cmsSaveProfileToMem(sub, NULL, &SubSize) || SubSize == 0) goto Cleanup; + + SubMem = (cmsUInt8Number*) malloc(SubSize); + if (SubMem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(sub, SubMem, &SubSize)) { + + Fail("Could not save the authored sub-profile"); + goto Cleanup; + } + + // ---- wrap it in an ICC.1 profile's ICC5 tag ---- + outer = cmsCreate_sRGBProfileTHR(DbgThread()); + if (outer == NULL) goto Cleanup; + + { + cmsICCData* Wrapper = (cmsICCData*) malloc(sizeof(cmsICCData) + SubSize); + + if (Wrapper == NULL) goto Cleanup; + + Wrapper ->len = SubSize; + Wrapper ->flag = 0; + memcpy(Wrapper ->data, SubMem, SubSize); + + if (!cmsWriteTag(outer, cmsSigEmbeddedV5ProfileTag, Wrapper)) { + + Fail("Could not write the ICC5 tag"); + free(Wrapper); + goto Cleanup; + } + + free(Wrapper); + } + + if (!cmsSaveProfileToMem(outer, NULL, &OuterSize) || OuterSize == 0) goto Cleanup; + + OuterMem = (cmsUInt8Number*) malloc(OuterSize); + if (OuterMem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(outer, OuterMem, &OuterSize)) goto Cleanup; + + // ---- read the whole thing back ---- + reopened = cmsOpenProfileFromMemTHR(DbgThread(), OuterMem, OuterSize); + if (reopened == NULL) { + + Fail("Could not reopen the authored outer profile"); + goto Cleanup; + } + + Embedded = (cmsICCData*) cmsReadTag(reopened, cmsSigEmbeddedV5ProfileTag); + if (Embedded == NULL || Embedded ->len != SubSize) { + + Fail("Authored ICC5 tag did not survive: len %d, expected %d", + Embedded == NULL ? 0 : Embedded ->len, SubSize); + goto Cleanup; + } + + extracted = cmsOpenProfileFromMemTHR(DbgThread(), Embedded ->data, Embedded ->len); + if (extracted == NULL) { + + Fail("Could not open the authored sub-profile out of the ICC5 tag"); + goto Cleanup; + } + + if (cmsGetSpectralPCS(extracted) != (0x72730000u | Channels)) { + + Fail("Authored spectral PCS came back as %x, expected %x", + cmsGetSpectralPCS(extracted), 0x72730000u | Channels); + goto Cleanup; + } + + { + cmsFloatArray* gotW = (cmsFloatArray*) cmsReadTag(extracted, cmsSigSpectralWhitePointTag); + cmsSpectralViewingConditions* gotV = + (cmsSpectralViewingConditions*) cmsReadTag(extracted, cmsSigSpectralViewingConditionsTag); + + if (gotW == NULL || gotW ->nValues != Channels) { + + Fail("Authored swpt did not survive"); + goto Cleanup; + } + + for (i = 0; i < Channels; i++) { + + if (fabs(gotW ->Values[i] - w ->Values[i]) > 1E-6) { + + Fail("Authored swpt value %d changed", i); + goto Cleanup; + } + } + + if (gotV == NULL || gotV ->ObserverSteps != 3 || gotV ->IlluminantSteps != 3 || + gotV ->IlluminantType != 1) { + + Fail("Authored svcn did not survive"); + goto Cleanup; + } + + // Dimensions alone are not proof the arrays made it through the ICC5 + // wrapper -- check one value out of each. Observer[0] was written as + // (0+1) = 1.0, Illuminant[0] as (100+0) = 100.0. + if (gotV ->Observer == NULL || fabs(gotV ->Observer[0] - 1.0) > 1E-6) { + + Fail("Authored svcn Observer[0] did not survive: got %f, expected 1.0", + gotV ->Observer == NULL ? -1.0 : gotV ->Observer[0]); + goto Cleanup; + } + + if (gotV ->Illuminant == NULL || fabs(gotV ->Illuminant[0] - 100.0) > 1E-6) { + + Fail("Authored svcn Illuminant[0] did not survive: got %f, expected 100.0", + gotV ->Illuminant == NULL ? -1.0 : gotV ->Illuminant[0]); + goto Cleanup; + } + } + + // A bare non-NULL check here would not catch the identity CLUT silently + // degrading to the zero-filled grid a naive DToB3 stage would produce, so + // evaluate the round-tripped pipeline at both exact grid corners of the 2 node + // per side float CLUT -- 0.0 and 1.0, where identity must hold exactly to + // float precision -- plus an interior point, where linear interpolation of a + // genuinely identity-shaped grid is still exact. + { + cmsPipeline* gotLut = (cmsPipeline*) cmsReadTag(extracted, cmsSigDToB3Tag); + cmsFloat32Number Probe[3][4] = { + { 0.0f, 0.0f, 0.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f }, + { 0.37f, 0.62f, 0.05f, 0.91f } + }; + cmsUInt32Number p; + + if (gotLut == NULL) { + + Fail("Authored DToB3 did not survive"); + goto Cleanup; + } + + for (p = 0; p < 3; p++) { + + cmsFloat32Number Out[MAX_STAGE_CHANNELS]; + + memset(Out, 0, sizeof(Out)); + cmsPipelineEvalFloat(Probe[p], Out, gotLut); + + for (i = 0; i < Channels; i++) { + + if (fabs(Out[i] - Probe[p][i]) > 1E-4) { + + Fail("Authored DToB3 probe %d channel %d: got %f, expected %f", + p, i, Out[i], Probe[p][i]); + goto Cleanup; + } + } + } + } + + rc = 1; + +Cleanup: + if (lut != NULL) cmsPipelineFree(lut); + cmsFreeFloatArray(w); + cmsFreeSpectralViewingConditions(sv); + if (SubMem != NULL) free(SubMem); + if (OuterMem != NULL) free(OuterMem); + if (extracted != NULL) cmsCloseProfile(extracted); + if (reopened != NULL) cmsCloseProfile(reopened); + if (outer != NULL) cmsCloseProfile(outer); + if (sub != NULL) cmsCloseProfile(sub); + + return rc; +} + +// Builds a one-segment curve of a given iccMAX formula type and evaluates it. +// nProbes is the number of (In, Expected) pairs to check; all 10 Params slots +// are always copied into the segment regardless of how many a given type uses. +static +cmsInt32Number TryFormulaSegment(cmsInt32Number Type, + const cmsFloat64Number* Params, + const cmsFloat32Number* In, + const cmsFloat32Number* Expected, + cmsUInt32Number nProbes, + cmsFloat64Number Tolerance) +{ + cmsCurveSegment Seg[1]; + cmsToneCurve* Curve; + cmsUInt32Number i; + + memset(Seg, 0, sizeof(Seg)); + + Seg[0].x0 = -1e22f; + Seg[0].x1 = 1e22f; + Seg[0].Type = Type; + for (i = 0; i < 10; i++) + Seg[0].Params[i] = Params[i]; + + Curve = cmsBuildSegmentedToneCurve(DbgThread(), 1, Seg); + if (Curve == NULL) { + Fail("cmsBuildSegmentedToneCurve failed for type %d", Type); + return 0; + } + + for (i = 0; i < nProbes; i++) { + + cmsFloat32Number got = cmsEvalToneCurveFloat(Curve, In[i]); + + // isnan() must be checked before the tolerance comparison: every relational + // comparison involving NaN is false under IEEE 754, so "fabs(got - Expected[i]) + // > Tolerance" would silently pass a NaN result instead of failing it. + if (isnan(got)) { + Fail("type %d at %f: got NaN, expected %f", Type, In[i], Expected[i]); + cmsFreeToneCurve(Curve); + return 0; + } + + if (fabs(got - Expected[i]) > Tolerance) { + Fail("type %d at %f: got %f, expected %f", Type, In[i], got, Expected[i]); + cmsFreeToneCurve(Curve); + return 0; + } + } + + cmsFreeToneCurve(Curve); + return 1; +} + +static +cmsInt32Number CheckICCMAXFormulaSegments(void) +{ + // Type 9: Y = a*(b*X + c)^g + d g=2 a=3 b=1 c=0 d=1 + { + static const cmsFloat64Number P[10] = { 2, 3, 1, 0, 1, 0, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 2.0f, 0.0f }; + static const cmsFloat32Number Out[] = { 13.0f, 1.0f }; + + if (!TryFormulaSegment(9, P, In, Out, 2, 1e-5)) return 0; + } + + // Type 10: Y = a*ln(d*X^g - b) + c g=1 a=2 b=0 c=5 d=1 + { + static const cmsFloat64Number P[10] = { 1, 2, 0, 5, 1, 0, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f, 0.25f }; + static const cmsFloat32Number Out[] = { 5.0f, 2.2274113f }; + + if (!TryFormulaSegment(10, P, In, Out, 2, 1e-5)) return 0; + } + + // Type 11: Y = e*exp((d*X^g - c)/a) + b g=1 a=2 b=1 c=0 d=2 e=3 + { + static const cmsFloat64Number P[10] = { 1, 2, 1, 0, 2, 3, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 0.0f, 1.0f }; + static const cmsFloat32Number Out[] = { 4.0f, 9.1548455f }; + + if (!TryFormulaSegment(11, P, In, Out, 2, 1e-5)) return 0; + } + + // Type 12: Y = d*(max(e*X^g - a,0)/(b - c*X^g))^w w=2 g=1 a=0 b=2 c=0 d=1 e=1 + // Chosen so transposing omega and gamma would give 0.5 at X=1, not 0.25 + { + static const cmsFloat64Number P[10] = { 2, 1, 0, 2, 0, 1, 1, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f, 2.0f }; + static const cmsFloat32Number Out[] = { 0.25f, 1.0f }; + + if (!TryFormulaSegment(12, P, In, Out, 2, 1e-5)) return 0; + } + + // Type 13: Y = d*((a + b*X^g)/(1 + c*X^g))^w w=2 g=1 a=0 b=1 c=1 d=1 + // Same guard against a transposed omega/gamma: it would give 0.5 at X=1 + { + static const cmsFloat64Number P[10] = { 2, 1, 0, 1, 1, 1, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f, 3.0f }; + static const cmsFloat32Number Out[] = { 0.25f, 0.5625f }; + + if (!TryFormulaSegment(13, P, In, Out, 2, 1e-5)) return 0; + } + + return 1; +} + +// Drives every degenerate branch on purpose and asserts the documented finite +// fallback, not merely that nothing crashed. +static +cmsInt32Number CheckICCMAXFormulaDegenerate(void) +{ + // Type 9 with b*X + c negative -> d + { + static const cmsFloat64Number P[10] = { 0.5, 3, 1, 0, 7, 0, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { -1.0f }; + static const cmsFloat32Number Out[] = { 7.0f }; + + if (!TryFormulaSegment(9, P, In, Out, 1, 1e-5)) return 0; + } + + // Type 10 with a non-positive log argument -> c + { + static const cmsFloat64Number P[10] = { 1, 2, 5, 11, 1, 0, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f }; + static const cmsFloat32Number Out[] = { 11.0f }; + + if (!TryFormulaSegment(10, P, In, Out, 1, 1e-5)) return 0; + } + + // Type 11 with a == 0 -> b + { + static const cmsFloat64Number P[10] = { 1, 0, 13, 0, 2, 3, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 0.5f }; + static const cmsFloat32Number Out[] = { 13.0f }; + + if (!TryFormulaSegment(11, P, In, Out, 1, 1e-5)) return 0; + } + + // Type 12 with a zero denominator (b - c*X^g == 0 at X = 1) -> 0 + { + static const cmsFloat64Number P[10] = { 2, 1, 0, 1, 1, 5, 1, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f }; + static const cmsFloat32Number Out[] = { 0.0f }; + + if (!TryFormulaSegment(12, P, In, Out, 1, 1e-5)) return 0; + } + + // Type 13 with a zero denominator (1 + c*X^g == 0 at X = 1, c = -1) -> 0 + { + static const cmsFloat64Number P[10] = { 2, 1, 0, 1, -1, 5, 0, 0, 0, 0 }; + static const cmsFloat32Number In[] = { 1.0f }; + static const cmsFloat32Number Out[] = { 0.0f }; + + if (!TryFormulaSegment(13, P, In, Out, 1, 1e-5)) return 0; + } + + return 1; +} + +// Writes a segmented curve holding one segment of each iccMAX formula type into a +// DToB3 mpet, reloads it, and checks the type and every parameter survived. This is +// what exercises ParamsByType on both sides: a wrong count there would misread the +// following segment's bytes rather than fail cleanly. It then corrupts the serialized +// bytes so the first segment claims ICC function type 8 and confirms the reader still +// rejects it at the wire level, exactly as it does for the in-memory writer path. +static +cmsInt32Number CheckICCMAXFormulaRoundTrip(void) +{ + static const cmsInt32Number Types[5] = { 9, 10, 11, 12, 13 }; + static const cmsUInt32Number Counts[5] = { 5, 5, 6, 7, 6 }; + static const cmsFloat64Number P[5][10] = { + { 2, 3, 1, 0, 1, 0, 0, 0, 0, 0 }, + { 1, 2, 0, 5, 1, 0, 0, 0, 0, 0 }, + { 1, 2, 1, 0, 2, 3, 0, 0, 0, 0 }, + { 2, 1, 0, 2, 0, 1, 1, 0, 0, 0 }, + { 2, 1, 0, 1, 1, 1, 0, 0, 0, 0 } + }; + static const cmsUInt8Number FormulaSeg[4] = { 0x70, 0x61, 0x72, 0x66 }; // 'parf' + + cmsHPROFILE h = NULL; + cmsHPROFILE h2 = NULL; + cmsHPROFILE h3 = NULL; + cmsPipeline* pipe = NULL; + cmsPipeline* ReadPipe; + cmsPipeline* BadPipe; + cmsToneCurve* Curves[1]; + cmsCurveSegment Seg[5]; + cmsStage* stage; + _cmsStageToneCurvesData* Data; + cmsUInt32Number clen = 0; + cmsUInt32Number i, j; + cmsUInt32Number rc = 0; + cmsUInt32Number sigOffset; + char* data = NULL; + char* corrupted = NULL; + + // One curve, five segments, one per formula type. Breakpoints are arbitrary but + // must be increasing; the values are not evaluated here, only round-tripped. + memset(Seg, 0, sizeof(Seg)); + for (i = 0; i < 5; i++) { + + Seg[i].x0 = (i == 0) ? -1e22f : (cmsFloat32Number) i; + Seg[i].x1 = (i == 4) ? 1e22f : (cmsFloat32Number) (i + 1); + Seg[i].Type = Types[i]; + + for (j = 0; j < 10; j++) + Seg[i].Params[j] = P[i][j]; + } + + Curves[0] = cmsBuildSegmentedToneCurve(DbgThread(), 5, Seg); + if (Curves[0] == NULL) { Fail("cmsBuildSegmentedToneCurve failed"); return 0; } + + pipe = cmsPipelineAlloc(DbgThread(), 1, 1); + if (pipe == NULL) { Fail("cmsPipelineAlloc failed"); goto Error; } + + if (!cmsPipelineInsertStage(pipe, cmsAT_END, cmsStageAllocToneCurves(DbgThread(), 1, Curves))) { + Fail("cmsStageAllocToneCurves failed"); + goto Error; + } + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) { Fail("cmsCreateProfilePlaceholder failed"); goto Error; } + + cmsSetProfileVersion(h, 5.0); + cmsSetDeviceClass(h, cmsSigOutputClass); + cmsSetColorSpace(h, cmsSigGrayData); + cmsSetPCS(h, cmsSigLabData); + + if (!cmsWriteTag(h, cmsSigDToB3Tag, pipe)) { Fail("cmsWriteTag DToB3 failed"); goto Error; } + + if (!cmsSaveProfileToMem(h, NULL, &clen)) { Fail("cmsSaveProfileToMem size failed"); goto Error; } + data = (char*) malloc(clen); + if (data == NULL) { Fail("malloc failed"); goto Error; } + if (!cmsSaveProfileToMem(h, data, &clen)) { Fail("cmsSaveProfileToMem failed"); goto Error; } + + h2 = cmsOpenProfileFromMem(data, clen); + if (h2 == NULL) { Fail("cmsOpenProfileFromMem failed"); goto Error; } + + ReadPipe = (cmsPipeline*) cmsReadTag(h2, cmsSigDToB3Tag); + if (ReadPipe == NULL) { Fail("cmsReadTag DToB3 returned NULL"); goto Error; } + + stage = cmsPipelineGetPtrToFirstStage(ReadPipe); + if (stage == NULL || cmsStageType(stage) != cmsSigCurveSetElemType) { + Fail("First stage is not a curve set"); + goto Error; + } + + Data = (_cmsStageToneCurvesData*) stage ->Data; + if (Data ->TheCurves[0] ->nSegments != 5) { + Fail("Got %d segments, expected 5", Data ->TheCurves[0] ->nSegments); + goto Error; + } + + for (i = 0; i < 5; i++) { + + cmsCurveSegment* s = &Data ->TheCurves[0] ->Segments[i]; + + if (s ->Type != Types[i]) { + Fail("Segment %d: type %d, expected %d", i, s ->Type, Types[i]); + goto Error; + } + + for (j = 0; j < Counts[i]; j++) { + + if (fabs(s ->Params[j] - P[i][j]) > 1e-5) { + Fail("Segment %d param %d: got %f, expected %f", i, j, s ->Params[j], P[i][j]); + goto Error; + } + } + } + + // Wire-level negative case: hand-corrupt the serialized bytes so the first + // formula segment (stored on the wire as ICC function type 9 - 6 = 3) instead + // claims ICC function type 8, one past the highest type this branch supports + // (7), and confirm the reader still rejects it. Rejection goes through + // cmsSignalError(cmsERROR_CORRUPTION_DETECTED, ...), which the installed + // FatalErrorQuit handler treats as fatal, so it is swapped out for the + // non-fatal ErrorReportingFunction for the duration of this probe, exactly as + // other negative-case tests in this file do (e.g. CheckSingleSampledCurve). + sigOffset = (cmsUInt32Number) -1; + for (i = 0; i + 4 <= clen; i++) { + if (memcmp(data + i, FormulaSeg, 4) == 0) { sigOffset = i; break; } + } + if (sigOffset == (cmsUInt32Number) -1) { + Fail("Could not locate a formula curve segment signature in the serialized profile"); + goto Error; + } + + corrupted = (char*) malloc(clen); + if (corrupted == NULL) { Fail("malloc failed"); goto Error; } + memcpy(corrupted, data, clen); + + // Signature (4 bytes) + reserved (4 bytes) + Type (2 bytes, big endian) ... + corrupted[sigOffset + 8] = (char) 0x00; + corrupted[sigOffset + 9] = (char) 0x08; + + cmsSetLogErrorHandler(ErrorReportingFunction); + TrappedError = FALSE; + SimultaneousErrors = 0; + + BadPipe = NULL; + h3 = cmsOpenProfileFromMem(corrupted, clen); + if (h3 != NULL) { + BadPipe = (cmsPipeline*) cmsReadTag(h3, cmsSigDToB3Tag); + } + + cmsSetLogErrorHandler(FatalErrorQuit); + TrappedError = FALSE; + SimultaneousErrors = 0; + + if (h3 != NULL && BadPipe != NULL) { + Fail("ICC function type 8 was not rejected at the wire level"); + goto Error; + } + + rc = 1; + +Error: + if (h != NULL) cmsCloseProfile(h); + if (h2 != NULL) cmsCloseProfile(h2); + if (h3 != NULL) cmsCloseProfile(h3); + if (pipe != NULL) cmsPipelineFree(pipe); + if (Curves[0] != NULL) cmsFreeToneCurve(Curves[0]); + if (data != NULL) free(data); + if (corrupted != NULL) free(corrupted); + return rc; +} + +// ICC.2 Table 113 bounds a clutElement's input count (P <= 16) but places no bound at all +// on its output count -- the spec pins Q only where it carries meaning, as in +// emissionCLUTElement and reflectanceCLUTElement, which both require Q = 3. Multi process +// elements are not constrained by the 16 channel ceiling that applies to a profile's +// colour spaces. So a spectral transform may be built from a plain 'clut' rather than the +// extended 'xclt', and such a profile must load. +static +cmsInt32Number CheckWidePlainClutElement(void) +{ + cmsHPROFILE h = NULL; + cmsPipeline* lut = NULL; + cmsPipeline* got; + cmsUInt8Number* Mem = NULL; + cmsUInt32Number Size = 0; + cmsInt32Number rc = 0; + const cmsUInt32Number In = 4, Out = 36; + + h = cmsCreateProfilePlaceholder(DbgThread()); + if (h == NULL) return 0; + + cmsSetProfileVersion(h, 5.0); + + lut = cmsPipelineAlloc(DbgThread(), In, Out); + if (lut == NULL) goto Cleanup; + + // A plain clutElement, not an extendedCLUTElement: 2 grid points per axis, 36 outputs + if (!cmsPipelineInsertStage(lut, cmsAT_BEGIN, + cmsStageAllocCLutFloat(DbgThread(), 2, In, Out, NULL))) { + + Fail("Could not build a wide plain clut pipeline"); + goto Cleanup; + } + + if (!cmsWriteTag(h, cmsSigDToB3Tag, lut)) { + + Fail("Could not write a wide plain clut into DToB3"); + goto Cleanup; + } + + if (!cmsSaveProfileToMem(h, NULL, &Size) || Size == 0) goto Cleanup; + + Mem = (cmsUInt8Number*) malloc(Size); + if (Mem == NULL) goto Cleanup; + + if (!cmsSaveProfileToMem(h, Mem, &Size)) goto Cleanup; + + cmsCloseProfile(h); + + h = cmsOpenProfileFromMemTHR(DbgThread(), Mem, Size); + if (h == NULL) { + + Fail("Could not reopen the wide plain clut profile"); + goto Cleanup; + } + + // Swap the fatal handler out around the read. If this ever regresses, the reader + // signals a corrupted tag, and under FatalErrorQuit that would kill the whole run + // partway through instead of reporting a failure here -- which would also make the + // Fail below unreachable. + cmsSetLogErrorHandler(ErrorReportingFunction); + got = (cmsPipeline*) cmsReadTag(h, cmsSigDToB3Tag); + cmsSetLogErrorHandler(FatalErrorQuit); + + if (got == NULL) { + + Fail("A profile with a wide plain clutElement failed to load"); + goto Cleanup; + } + + if (cmsPipelineInputChannels(got) != In || cmsPipelineOutputChannels(got) != Out) { + + Fail("Wide plain clut came back as %d to %d channels, expected %d to %d", + cmsPipelineInputChannels(got), cmsPipelineOutputChannels(got), In, Out); + goto Cleanup; + } + + rc = 1; + +Cleanup: + if (lut != NULL) cmsPipelineFree(lut); + if (Mem != NULL) free(Mem); + if (h != NULL) cmsCloseProfile(h); + + return rc; +} + +#endif /* CMS_USE_ICCMAX_SPECTRAL */ + // ----------------------------------------------------------------------------------------------------- @@ -9799,6 +12684,28 @@ int main(int argc, char* argv[]) Check("Named color lists", CheckNamedColorList); Check("Create named color profile", CreateNamedColorProfile); + // CLUT allocation limits + Check("CLUT overflow rejected", CheckCLUTOverflowRejected); + + // iccMAX (ICC.2) +#ifdef CMS_USE_ICCMAX_SPECTRAL + Check("Float16 IO", CheckFloat16IO); + Check("Extended CLUT element", CheckExtCLutElement); + Check("ExtCLUT all encoding types", CheckExtCLutAllEncodings); + Check("High channel count pipeline", CheckHighChannelPipeline); + Check("Spectral PCS preservation", CheckSpectralPCSPreservation); + Check("Single sampled curve", CheckSingleSampledCurve); + Check("Single sampled curve round trip", CheckSingleSampledCurveRoundTrip); + Check("Hybrid printer profile", CheckHybridPrinterProfile); + Check("Spectral white point round trip", CheckSpectralWhitePointRoundTrip); + Check("Spectral viewing conditions round trip", CheckSpectralViewingConditionsRoundTrip); + Check("Spectral tags against fixture", CheckSpectralTagsAgainstFixture); + Check("Author a hybrid printer profile", CheckAuthorHybridProfile); + Check("iccMAX formula segments", CheckICCMAXFormulaSegments); + Check("iccMAX formula degenerate cases", CheckICCMAXFormulaDegenerate); + Check("iccMAX formula round trip", CheckICCMAXFormulaRoundTrip); + Check("Wide plain clut element", CheckWidePlainClutElement); +#endif // Profile I/O (this one is huge!) Check("Profile creation", CheckProfileCreation);