//--------------------------------------------------------------------------------- // // Little Color Management System // Copyright (c) 1998-2023 Marti Maria Saguer // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the Software // is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // //--------------------------------------------------------------------------------- // #include "lcms2_internal.h" // PostScript ColorRenderingDictionary and ColorSpaceArray #define MAXPSCOLS … /* Implementation -------------- PostScript does use XYZ as its internal PCS. But since PostScript interpolation tables are limited to 8 bits, I use Lab as a way to improve the accuracy, favoring perceptual results. So, for the creation of each CRD, CSA the profiles are converted to Lab via a device link between profile -> Lab or Lab -> profile. The PS code necessary to convert Lab <-> XYZ is also included. Color Space Arrays (CSA) ================================================================================== In order to obtain precision, code chooses between three ways to implement the device -> XYZ transform. These cases identifies monochrome profiles (often implemented as a set of curves), matrix-shaper and Pipeline-based. Monochrome ----------- This is implemented as /CIEBasedA CSA. The prelinearization curve is placed into /DecodeA section, and matrix equals to D50. Since here is no interpolation tables, I do the conversion directly to XYZ NOTE: CLUT-based monochrome profiles are NOT supported. So, cmsFLAGS_MATRIXINPUT flag is forced on such profiles. [ /CIEBasedA << /DecodeA { transfer function } bind /MatrixA [D50] /RangeLMN [ 0.0 cmsD50X 0.0 cmsD50Y 0.0 cmsD50Z ] /WhitePoint [D50] /BlackPoint [BP] /RenderingIntent (intent) >> ] On simpler profiles, the PCS is already XYZ, so no conversion is required. Matrix-shaper based ------------------- This is implemented both with /CIEBasedABC or /CIEBasedDEF depending on the profile implementation. Since here there are no interpolation tables, I do the conversion directly to XYZ [ /CIEBasedABC << /DecodeABC [ {transfer1} {transfer2} {transfer3} ] /MatrixABC [Matrix] /RangeLMN [ 0.0 cmsD50X 0.0 cmsD50Y 0.0 cmsD50Z ] /DecodeLMN [ { / 2} dup dup ] /WhitePoint [D50] /BlackPoint [BP] /RenderingIntent (intent) >> ] CLUT based ---------- Lab is used in such cases. [ /CIEBasedDEF << /DecodeDEF [ <prelinearization> ] /Table [ p p p [<...>]] /RangeABC [ 0 1 0 1 0 1] /DecodeABC[ <postlinearization> ] /RangeLMN [ -0.236 1.254 0 1 -0.635 1.640 ] % -128/500 1+127/500 0 1 -127/200 1+128/200 /MatrixABC [ 1 1 1 1 0 0 0 0 -1] /WhitePoint [D50] /BlackPoint [BP] /RenderingIntent (intent) ] Color Rendering Dictionaries (CRD) ================================== These are always implemented as CLUT, and always are using Lab. Since CRD are expected to be used as resources, the code adds the definition as well. << /ColorRenderingType 1 /WhitePoint [ D50 ] /BlackPoint [BP] /MatrixPQR [ Bradford ] /RangePQR [-0.125 1.375 -0.125 1.375 -0.125 1.375 ] /TransformPQR [ {4 index 3 get div 2 index 3 get mul exch pop exch pop exch pop exch pop } bind {4 index 4 get div 2 index 4 get mul exch pop exch pop exch pop exch pop } bind {4 index 5 get div 2 index 5 get mul exch pop exch pop exch pop exch pop } bind ] /MatrixABC <...> /EncodeABC <...> /RangeABC <.. used for XYZ -> Lab> /EncodeLMN /RenderTable [ p p p [<...>]] /RenderingIntent (Perceptual) >> /Current exch /ColorRendering defineresource pop The following stages are used to convert from XYZ to Lab -------------------------------------------------------- Input is given at LMN stage on X, Y, Z Encode LMN gives us f(X/Xn), f(Y/Yn), f(Z/Zn) /EncodeLMN [ { 0.964200 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind { 1.000000 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind { 0.824900 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind ] MatrixABC is used to compute f(Y/Yn), f(X/Xn) - f(Y/Yn), f(Y/Yn) - f(Z/Zn) | 0 1 0| | 1 -1 0| | 0 1 -1| /MatrixABC [ 0 1 0 1 -1 1 0 0 -1 ] EncodeABC finally gives Lab values. /EncodeABC [ { 116 mul 16 sub 100 div } bind { 500 mul 128 add 255 div } bind { 200 mul 128 add 255 div } bind ] The following stages are used to convert Lab to XYZ ---------------------------------------------------- /RangeABC [ 0 1 0 1 0 1] /DecodeABC [ { 100 mul 16 add 116 div } bind { 255 mul 128 sub 500 div } bind { 255 mul 128 sub 200 div } bind ] /MatrixABC [ 1 1 1 1 0 0 0 0 -1] /DecodeLMN [ {dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse 0.964200 mul} bind {dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse } bind {dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse 0.824900 mul} bind ] */ /* PostScript algorithms discussion. ========================================================================================================= 1D interpolation algorithm 1D interpolation (float) ------------------------ val2 = Domain * Value; cell0 = (int) floor(val2); cell1 = (int) ceil(val2); rest = val2 - cell0; y0 = LutTable[cell0] ; y1 = LutTable[cell1] ; y = y0 + (y1 - y0) * rest; PostScript code Stack ================================================ { % v <check 0..1.0> [array] % v tab dup % v tab tab length 1 sub % v tab dom 3 -1 roll % tab dom v mul % tab val2 dup % tab val2 val2 dup % tab val2 val2 val2 floor cvi % tab val2 val2 cell0 exch % tab val2 cell0 val2 ceiling cvi % tab val2 cell0 cell1 3 index % tab val2 cell0 cell1 tab exch % tab val2 cell0 tab cell1 get % tab val2 cell0 y1 4 -1 roll % val2 cell0 y1 tab 3 -1 roll % val2 y1 tab cell0 get % val2 y1 y0 dup % val2 y1 y0 y0 3 1 roll % val2 y0 y1 y0 sub % val2 y0 (y1-y0) 3 -1 roll % y0 (y1-y0) val2 dup % y0 (y1-y0) val2 val2 floor cvi % y0 (y1-y0) val2 floor(val2) sub % y0 (y1-y0) rest mul % y0 t1 add % y 65535 div % result } bind */ // This struct holds the memory block currently being write cmsPsSamplerCargo; static int _cmsPSActualColumn = …; // Convert to byte static cmsUInt8Number Word2Byte(cmsUInt16Number w) { … } // Write a cooked byte static void WriteByte(cmsIOHANDLER* m, cmsUInt8Number b) { … } // ----------------------------------------------------------------- PostScript generation // Removes offending carriage returns static char* RemoveCR(const char* txt) { … } static void EmitHeader(cmsIOHANDLER* m, const char* Title, cmsHPROFILE hProfile) { … } // Emits White & Black point. White point is always D50, Black point is the device // Black point adapted to D50. static void EmitWhiteBlackD50(cmsIOHANDLER* m, cmsCIEXYZ* BlackPoint) { … } static void EmitRangeCheck(cmsIOHANDLER* m) { … } // Does write the intent static void EmitIntent(cmsIOHANDLER* m, cmsUInt32Number RenderingIntent) { … } // // Convert L* to Y // // Y = Yn*[ (L* + 16) / 116] ^ 3 if (L*) >= 6 / 29 // = Yn*( L* / 116) / 7.787 if (L*) < 6 / 29 // // Lab -> XYZ, see the discussion above static void EmitLab2XYZ(cmsIOHANDLER* m) { … } static void EmitSafeGuardBegin(cmsIOHANDLER* m, const char* name) { … } static void EmitSafeGuardEnd(cmsIOHANDLER* m, const char* name, int depth) { … } // Outputs a table of words. It does use 16 bits static void Emit1Gamma(cmsIOHANDLER* m, cmsToneCurve* Table, const char* name) { … } // Compare gamma table static cmsBool GammaTableEquals(cmsUInt16Number* g1, cmsUInt16Number* g2, cmsUInt32Number nG1, cmsUInt32Number nG2) { … } // Does write a set of gamma curves static void EmitNGamma(cmsIOHANDLER* m, cmsUInt32Number n, cmsToneCurve* g[], const char* nameprefix) { … } // Following code dumps a LUT onto memory stream // This is the sampler. Intended to work in SAMPLER_INSPECT mode, // that is, the callback will be called for each knot with // // In[] The grid location coordinates, normalized to 0..ffff // Out[] The Pipeline values, normalized to 0..ffff // // Returning a value other than 0 does terminate the sampling process // // Each row contains Pipeline values for all but first component. So, I // detect row changing by keeping a copy of last value of first // component. -1 is used to mark beginning of whole block. static int OutputValueSampler(CMSREGISTER const cmsUInt16Number In[], CMSREGISTER cmsUInt16Number Out[], CMSREGISTER void* Cargo) { … } // Writes a Pipeline on memstream. Could be 8 or 16 bits based static void WriteCLUT(cmsIOHANDLER* m, cmsStage* mpe, const char* PreMaj, const char* PostMaj, const char* PreMin, const char* PostMin, int FixWhite, cmsColorSpaceSignature ColorSpace) { … } // Dumps CIEBasedA Color Space Array static int EmitCIEBasedA(cmsIOHANDLER* m, cmsToneCurve* Curve, cmsCIEXYZ* BlackPoint) { … } // Dumps CIEBasedABC Color Space Array static int EmitCIEBasedABC(cmsIOHANDLER* m, cmsFloat64Number* Matrix, cmsToneCurve** CurveSet, cmsCIEXYZ* BlackPoint) { … } static int EmitCIEBasedDEF(cmsIOHANDLER* m, cmsPipeline* Pipeline, cmsUInt32Number Intent, cmsCIEXYZ* BlackPoint) { … } // Generates a curve from a gray profile static cmsToneCurve* ExtractGray2Y(cmsContext ContextID, cmsHPROFILE hProfile, cmsUInt32Number Intent) { … } // Because PostScript has only 8 bits in /Table, we should use // a more perceptually uniform space... I do choose Lab. static int WriteInputLUT(cmsIOHANDLER* m, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { … } static cmsFloat64Number* GetPtrToMatrix(const cmsStage* mpe) { … } // Does create CSA based on matrix-shaper. Allowed types are gray and RGB based static int WriteInputMatrixShaper(cmsIOHANDLER* m, cmsHPROFILE hProfile, cmsStage* Matrix, cmsStage* Shaper) { … } // Creates a PostScript color list from a named profile data. // This is a HP extension, and it works in Lab instead of XYZ static int WriteNamedColorCSA(cmsIOHANDLER* m, cmsHPROFILE hNamedColor, cmsUInt32Number Intent) { … } // Does create a Color Space Array on XYZ colorspace for PostScript usage static cmsUInt32Number GenerateCSA(cmsContext ContextID, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags, cmsIOHANDLER* mem) { … } // ------------------------------------------------------ Color Rendering Dictionary (CRD) /* Black point compensation plus chromatic adaptation: Step 1 - Chromatic adaptation ============================= WPout X = ------- PQR Wpin Step 2 - Black point compensation ================================= (WPout - BPout)*X - WPout*(BPin - BPout) out = --------------------------------------- WPout - BPin Algorithm discussion ==================== TransformPQR(WPin, BPin, WPout, BPout, PQR) Wpin,etc= { Xws Yws Zws Pws Qws Rws } Algorithm Stack 0...n =========================================================== PQR BPout WPout BPin WPin 4 index 3 get WPin PQR BPout WPout BPin WPin div (PQR/WPin) BPout WPout BPin WPin 2 index 3 get WPout (PQR/WPin) BPout WPout BPin WPin mult WPout*(PQR/WPin) BPout WPout BPin WPin 2 index 3 get WPout WPout*(PQR/WPin) BPout WPout BPin WPin 2 index 3 get BPout WPout WPout*(PQR/WPin) BPout WPout BPin WPin sub (WPout-BPout) WPout*(PQR/WPin) BPout WPout BPin WPin mult (WPout-BPout)* WPout*(PQR/WPin) BPout WPout BPin WPin 2 index 3 get WPout (BPout-WPout)* WPout*(PQR/WPin) BPout WPout BPin WPin 4 index 3 get BPin WPout (BPout-WPout)* WPout*(PQR/WPin) BPout WPout BPin WPin 3 index 3 get BPout BPin WPout (BPout-WPout)* WPout*(PQR/WPin) BPout WPout BPin WPin sub (BPin-BPout) WPout (BPout-WPout)* WPout*(PQR/WPin) BPout WPout BPin WPin mult (BPin-BPout)*WPout (BPout-WPout)* WPout*(PQR/WPin) BPout WPout BPin WPin sub (BPout-WPout)* WPout*(PQR/WPin)-(BPin-BPout)*WPout BPout WPout BPin WPin 3 index 3 get BPin (BPout-WPout)* WPout*(PQR/WPin)-(BPin-BPout)*WPout BPout WPout BPin WPin 3 index 3 get WPout BPin (BPout-WPout)* WPout*(PQR/WPin)-(BPin-BPout)*WPout BPout WPout BPin WPin exch sub (WPout-BPin) (BPout-WPout)* WPout*(PQR/WPin)-(BPin-BPout)*WPout BPout WPout BPin WPin div exch pop exch pop exch pop exch pop */ static void EmitPQRStage(cmsIOHANDLER* m, cmsHPROFILE hProfile, int DoBPC, int lIsAbsolute) { … } static void EmitXYZ2Lab(cmsIOHANDLER* m) { … } // Due to impedance mismatch between XYZ and almost all RGB and CMYK spaces // I choose to dump LUTS in Lab instead of XYZ. There is still a lot of wasted // space on 3D CLUT, but since space seems not to be a problem here, 33 points // would give a reasonable accuracy. Note also that CRD tables must operate in // 8 bits. static int WriteOutputLUT(cmsIOHANDLER* m, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { … } // Builds a ASCII string containing colorant list in 0..1.0 range static void BuildColorantList(char *Colorant, cmsUInt32Number nColorant, cmsUInt16Number Out[]) { … } // Creates a PostScript color list from a named profile data. // This is a HP extension. static int WriteNamedColorCRD(cmsIOHANDLER* m, cmsHPROFILE hNamedColor, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { … } // This one does create a Color Rendering Dictionary. // CRD are always LUT-Based, no matter if profile is // implemented as matrix-shaper. static cmsUInt32Number GenerateCRD(cmsContext ContextID, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags, cmsIOHANDLER* mem) { … } cmsUInt32Number CMSEXPORT cmsGetPostScriptColorResource(cmsContext ContextID, cmsPSResourceType Type, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags, cmsIOHANDLER* io) { … } cmsUInt32Number CMSEXPORT cmsGetPostScriptCRD(cmsContext ContextID, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags, void* Buffer, cmsUInt32Number dwBufferLen) { … } // Does create a Color Space Array on XYZ colorspace for PostScript usage cmsUInt32Number CMSEXPORT cmsGetPostScriptCSA(cmsContext ContextID, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags, void* Buffer, cmsUInt32Number dwBufferLen) { … }