// // Little cms // Copyright (C) 1998-2007 Marti Maria // // 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. // Postscript level 2 operators #include "lcms.h" #include #include // PostScript ColorRenderingDictionary and ColorSpaceArray LCMSAPI DWORD LCMSEXPORT cmsGetPostScriptCSA(cmsHPROFILE hProfile, int Intent, LPVOID Buffer, DWORD dwBufferLen); LCMSAPI DWORD LCMSEXPORT cmsGetPostScriptCRD(cmsHPROFILE hProfile, int Intent, LPVOID Buffer, DWORD dwBufferLen); LCMSAPI DWORD LCMSEXPORT cmsGetPostScriptCRDEx(cmsHPROFILE hProfile, int Intent, DWORD dwFlags, LPVOID Buffer, DWORD dwBufferLen); // -------------------------------------------------------------------- Implementation #define MAXPSCOLS 60 // Columns on tables /* 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 precission, 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 LUT-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 D50X 0.0 D50Y 0.0 D50Z ] /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 on dependig of profile implementation. Since here is no interpolation tables, I do the conversion directly to XYZ [ /CIEBasedABC << /DecodeABC [ {transfer1} {transfer2} {transfer3} ] /MatrixABC [Matrix] /RangeLMN [ 0.0 D50X 0.0 D50Y 0.0 D50Z ] /DecodeLMN [ { / 2} dup dup ] /WhitePoint [D50] /BlackPoint [BP] /RenderingIntent (intent) >> ] CLUT based ---------- Lab is used in such cases. [ /CIEBasedDEF << /DecodeDEF [ ] /Table [ p p p [<...>]] /RangeABC [ 0 1 0 1 0 1] /DecodeABC[ ] /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 [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 */ static icTagSignature Device2PCSTab[] = {icSigAToB0Tag, // Perceptual icSigAToB1Tag, // Relative colorimetric icSigAToB2Tag, // Saturation icSigAToB1Tag }; // Absolute colorimetric // (Relative/WhitePoint) // --------------------------------------------------------------- Memory Stream // // This struct holds the memory block currently being write // typedef struct { LPBYTE Block; LPBYTE Ptr; DWORD dwMax; DWORD dwUsed; int MaxCols; int Col; int HasError; } MEMSTREAM, FAR* LPMEMSTREAM; typedef struct { LPLUT Lut; LPMEMSTREAM m; int FirstComponent; int SecondComponent; int bps; const char* PreMaj; const char* PostMaj; const char* PreMin; const char* PostMin; int lIsInput; // Handle L* encoding int FixWhite; // Force mapping of pure white icColorSpaceSignature ColorSpace; // ColorSpace of profile } SAMPLERCARGO, FAR* LPSAMPLERCARGO; // Creates a ready to use memory stream static LPMEMSTREAM CreateMemStream(LPBYTE Buffer, DWORD dwMax, int MaxCols) { LPMEMSTREAM m = (LPMEMSTREAM) _cmsMalloc(sizeof(MEMSTREAM)); if (m == NULL) return NULL; ZeroMemory(m, sizeof(MEMSTREAM)); m -> Block = m -> Ptr = Buffer; m -> dwMax = dwMax; m -> dwUsed = 0; m -> MaxCols = MaxCols; m -> Col = 0; m -> HasError = 0; return m; } // Convert to byte static BYTE Word2Byte(WORD w) { return (BYTE) floor((double) w / 257.0 + 0.5); } // Convert to byte (using ICC2 notation) static BYTE L2Byte(WORD w) { int ww = w + 0x0080; if (ww > 0xFFFF) return 0xFF; return (BYTE) ((WORD) (ww >> 8) & 0xFF); } // Write a raw, uncooked byte. Check for space static void WriteRawByte(LPMEMSTREAM m, BYTE b) { if (m -> dwUsed + 1 > m -> dwMax) { m -> HasError = 1; } if (!m ->HasError && m ->Block) { *m ->Ptr++ = b; } m -> dwUsed++; } // Write a cooked byte static void WriteByte(LPMEMSTREAM m, BYTE b) { static const BYTE Hex[] = "0123456789ABCDEF"; BYTE c; c = Hex[(b >> 4) & 0x0f]; WriteRawByte(m, c); c = Hex[b & 0x0f]; WriteRawByte(m, c); m -> Col += 2; if (m -> Col > m -> MaxCols) { WriteRawByte(m, '\n'); m -> Col = 0; } } // Does write a formatted string. Guaranteed to be 2048 bytes at most. static void Writef(LPMEMSTREAM m, const char *frm, ...) { va_list args; LPBYTE pt; BYTE Buffer[2048]; va_start(args, frm); vsnprintf((char*) Buffer, 2048, frm, args); for (pt = Buffer; *pt; pt++) { WriteRawByte(m, *pt); } va_end(args); } // ----------------------------------------------------------------- PostScript generation // Removes offending Carriage returns static char* RemoveCR(const char* txt) { static char Buffer[2048]; char* pt; strncpy(Buffer, txt, 2047); Buffer[2047] = 0; for (pt = Buffer; *pt; pt++) if (*pt == '\n' || *pt == '\r') *pt = ' '; return Buffer; } static void EmitHeader(LPMEMSTREAM m, const char* Title, cmsHPROFILE hProfile) { time_t timer; time(&timer); Writef(m, "%%!PS-Adobe-3.0\n"); Writef(m, "%%\n"); Writef(m, "%% %s\n", Title); Writef(m, "%% Source: %s\n", RemoveCR(cmsTakeProductName(hProfile))); Writef(m, "%% Description: %s\n", RemoveCR(cmsTakeProductDesc(hProfile))); Writef(m, "%% Created: %s", ctime(&timer)); // ctime appends a \n!!! Writef(m, "%%\n"); Writef(m, "%%%%BeginResource\n"); } // Emits White & Black point. White point is always D50, Black point is the device // Black point adapted to D50. static void EmitWhiteBlackD50(LPMEMSTREAM m, LPcmsCIEXYZ BlackPoint) { Writef(m, "/BlackPoint [%f %f %f]\n", BlackPoint -> X, BlackPoint -> Y, BlackPoint -> Z); Writef(m, "/WhitePoint [%f %f %f]\n", cmsD50_XYZ()->X, cmsD50_XYZ()->Y, cmsD50_XYZ()->Z); } static void EmitRangeCheck(LPMEMSTREAM m) { Writef(m, "dup 0.0 lt { pop 0.0 } if " "dup 1.0 gt { pop 1.0 } if "); } // Does write the intent static void EmitIntent(LPMEMSTREAM m, int RenderingIntent) { const char *intent; switch (RenderingIntent) { case INTENT_PERCEPTUAL: intent = "Perceptual"; break; case INTENT_RELATIVE_COLORIMETRIC: intent = "RelativeColorimetric"; break; case INTENT_ABSOLUTE_COLORIMETRIC: intent = "AbsoluteColorimetric"; break; case INTENT_SATURATION: intent = "Saturation"; break; default: intent = "Undefined"; break; } Writef(m, "/RenderingIntent (%s)\n", intent ); } // // Convert L* to Y // // Y = Yn*[ (L* + 16) / 116] ^ 3 if (L*) >= 6 / 29 // = Yn*( L* / 116) / 7.787 if (L*) < 6 / 29 // /* static void EmitL2Y(LPMEMSTREAM m) { Writef(m, "{ " "100 mul 16 add 116 div " // (L * 100 + 16) / 116 "dup 6 29 div ge " // >= 6 / 29 ? "{ dup dup mul mul } " // yes, ^3 and done "{ 4 29 div sub 108 841 div mul } " // no, slope limiting "ifelse } bind "); } */ // Lab -> XYZ, see the discussion above static void EmitLab2XYZ(LPMEMSTREAM m) { Writef(m, "/RangeABC [ 0 1 0 1 0 1]\n"); Writef(m, "/DecodeABC [\n"); Writef(m, "{100 mul 16 add 116 div } bind\n"); Writef(m, "{255 mul 128 sub 500 div } bind\n"); Writef(m, "{255 mul 128 sub 200 div } bind\n"); Writef(m, "]\n"); Writef(m, "/MatrixABC [ 1 1 1 1 0 0 0 0 -1]\n"); Writef(m, "/RangeLMN [ -0.236 1.254 0 1 -0.635 1.640 ]\n"); Writef(m, "/DecodeLMN [\n"); Writef(m, "{dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse 0.964200 mul} bind\n"); Writef(m, "{dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse } bind\n"); Writef(m, "{dup 6 29 div ge {dup dup mul mul} {4 29 div sub 108 841 div mul} ifelse 0.824900 mul} bind\n"); Writef(m, "]\n"); } // Outputs a table of words. It does use 16 bits static void Emit1Gamma(LPMEMSTREAM m, LPWORD Table, int nEntries) { int i; double gamma; if (nEntries <= 0) return; // Empty table // Suppress whole if identity if (cmsIsLinear(Table, nEntries)) return; // Check if is really an exponential. If so, emit "exp" gamma = cmsEstimateGammaEx(Table, nEntries, 0.001); if (gamma > 0) { Writef(m, "{ %g exp } bind ", gamma); return; } Writef(m, "{ "); // Bounds check EmitRangeCheck(m); // Emit intepolation code // PostScript code Stack // =============== ======================== // v Writef(m, " ["); // TODO: Check for endianess!!! for (i=0; i < nEntries; i++) { Writef(m, "%d ", Table[i]); } Writef(m, "] "); // v tab Writef(m, "dup "); // v tab tab Writef(m, "length 1 sub "); // v tab dom Writef(m, "3 -1 roll "); // tab dom v Writef(m, "mul "); // tab val2 Writef(m, "dup "); // tab val2 val2 Writef(m, "dup "); // tab val2 val2 val2 Writef(m, "floor cvi "); // tab val2 val2 cell0 Writef(m, "exch "); // tab val2 cell0 val2 Writef(m, "ceiling cvi "); // tab val2 cell0 cell1 Writef(m, "3 index "); // tab val2 cell0 cell1 tab Writef(m, "exch "); // tab val2 cell0 tab cell1 Writef(m, "get "); // tab val2 cell0 y1 Writef(m, "4 -1 roll "); // val2 cell0 y1 tab Writef(m, "3 -1 roll "); // val2 y1 tab cell0 Writef(m, "get "); // val2 y1 y0 Writef(m, "dup "); // val2 y1 y0 y0 Writef(m, "3 1 roll "); // val2 y0 y1 y0 Writef(m, "sub "); // val2 y0 (y1-y0) Writef(m, "3 -1 roll "); // y0 (y1-y0) val2 Writef(m, "dup "); // y0 (y1-y0) val2 val2 Writef(m, "floor cvi "); // y0 (y1-y0) val2 floor(val2) Writef(m, "sub "); // y0 (y1-y0) rest Writef(m, "mul "); // y0 t1 Writef(m, "add "); // y Writef(m, "65535 div "); // result Writef(m, " } bind "); } // Compare gamma table static LCMSBOOL GammaTableEquals(LPWORD g1, LPWORD g2, int nEntries) { return memcmp(g1, g2, nEntries* sizeof(WORD)) == 0; } // Does write a set of gamma curves static void EmitNGamma(LPMEMSTREAM m, int n, LPWORD g[], int nEntries) { int i; for( i=0; i < n; i++ ) { if (i > 0 && GammaTableEquals(g[i-1], g[i], nEntries)) { Writef(m, "dup "); } else { Emit1Gamma(m, g[i], nEntries); } } } // Check whatever a profile has CLUT tables (only on input) static LCMSBOOL IsLUTbased(cmsHPROFILE hProfile, int Intent) { icTagSignature Tag; // Check if adequate tag is present Tag = Device2PCSTab[Intent]; if (cmsIsTag(hProfile, Tag)) return 1; // If not present, revert to default (perceptual) Tag = icSigAToB0Tag; // If no tag present, try matrix-shaper return cmsIsTag(hProfile, Tag); } // 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 LUT values, normalized to 0..ffff // // Returning a value other than 0 does terminate the sampling process // // Each row contains LUT 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 begining of whole block. static int OutputValueSampler(register WORD In[], register WORD Out[], register LPVOID Cargo) { LPSAMPLERCARGO sc = (LPSAMPLERCARGO) Cargo; unsigned int i; if (sc -> FixWhite) { if (In[0] == 0xFFFF) { // Only in L* = 100, ab = [-8..8] if ((In[1] >= 0x7800 && In[1] <= 0x8800) && (In[2] >= 0x7800 && In[2] <= 0x8800)) { WORD* Black; WORD* White; int nOutputs; if (!_cmsEndPointsBySpace(sc ->ColorSpace, &White, &Black, &nOutputs)) return 0; for (i=0; i < (unsigned int) nOutputs; i++) Out[i] = White[i]; } } } // Hadle the parenthesis on rows if (In[0] != sc ->FirstComponent) { if (sc ->FirstComponent != -1) { Writef(sc ->m, sc ->PostMin); sc ->SecondComponent = -1; Writef(sc ->m, sc ->PostMaj); } // Begin block sc->m->Col = 0; Writef(sc ->m, sc ->PreMaj); sc ->FirstComponent = In[0]; } if (In[1] != sc ->SecondComponent) { if (sc ->SecondComponent != -1) { Writef(sc ->m, sc ->PostMin); } Writef(sc ->m, sc ->PreMin); sc ->SecondComponent = In[1]; } // Dump table. Could be Word or byte based on // depending on bps member (16 bps mode is not currently // being used at all, but is here for future ampliations) for (i=0; i < sc -> Lut ->OutputChan; i++) { WORD wWordOut = Out[i]; if (sc ->bps == 8) { // Value as byte BYTE wByteOut; // If is input, convert from Lab2 to Lab4 (just divide by 256) if (sc ->lIsInput) { wByteOut = L2Byte(wWordOut); } else wByteOut = Word2Byte(wWordOut); WriteByte(sc -> m, wByteOut); } else { // Value as word WriteByte(sc -> m, (BYTE) (wWordOut & 0xFF)); WriteByte(sc -> m, (BYTE) ((wWordOut >> 8) & 0xFF)); } } return 1; } // Writes a LUT on memstream. Could be 8 or 16 bits based static void WriteCLUT(LPMEMSTREAM m, LPLUT Lut, int bps, const char* PreMaj, const char* PostMaj, const char* PreMin, const char* PostMin, int lIsInput, int FixWhite, icColorSpaceSignature ColorSpace) { unsigned int i; SAMPLERCARGO sc; sc.FirstComponent = -1; sc.SecondComponent = -1; sc.Lut = Lut; sc.m = m; sc.bps = bps; sc.PreMaj = PreMaj; sc.PostMaj= PostMaj; sc.PreMin = PreMin; sc.PostMin = PostMin; sc.lIsInput = lIsInput; sc.FixWhite = FixWhite; sc.ColorSpace = ColorSpace; Writef(m, "["); for (i=0; i < Lut ->InputChan; i++) Writef(m, " %d ", Lut ->cLutPoints); Writef(m, " [\n"); cmsSample3DGrid(Lut, OutputValueSampler, (LPVOID) &sc, SAMPLER_INSPECT); Writef(m, PostMin); Writef(m, PostMaj); Writef(m, "] "); } // Dumps CIEBasedA Color Space Array static int EmitCIEBasedA(LPMEMSTREAM m, LPWORD Tab, int nEntries, LPcmsCIEXYZ BlackPoint) { Writef(m, "[ /CIEBasedA\n"); Writef(m, " <<\n"); Writef(m, "/DecodeA "); Emit1Gamma(m,Tab, nEntries); Writef(m, " \n"); Writef(m, "/MatrixA [ 0.9642 1.0000 0.8249 ]\n"); Writef(m, "/RangeLMN [ 0.0 0.9642 0.0 1.0000 0.0 0.8249 ]\n"); EmitWhiteBlackD50(m, BlackPoint); EmitIntent(m, INTENT_PERCEPTUAL); Writef(m, ">>\n"); Writef(m, "]\n"); return 1; } // Dumps CIEBasedABC Color Space Array static int EmitCIEBasedABC(LPMEMSTREAM m, LPWORD L[], int nEntries, LPWMAT3 Matrix, LPcmsCIEXYZ BlackPoint) { int i; Writef(m, "[ /CIEBasedABC\n"); Writef(m, "<<\n"); Writef(m, "/DecodeABC [ "); EmitNGamma(m, 3, L, nEntries); Writef(m, "]\n"); Writef(m, "/MatrixABC [ " ); for( i=0; i < 3; i++ ) { Writef(m, "%.6f %.6f %.6f ", FIXED_TO_DOUBLE(Matrix->v[0].n[i]), FIXED_TO_DOUBLE(Matrix->v[1].n[i]), FIXED_TO_DOUBLE(Matrix->v[2].n[i])); } Writef(m, "]\n"); Writef(m, "/RangeLMN [ 0.0 0.9642 0.0 1.0000 0.0 0.8249 ]\n"); EmitWhiteBlackD50(m, BlackPoint); EmitIntent(m, INTENT_PERCEPTUAL); Writef(m, ">>\n"); Writef(m, "]\n"); return 1; } static int EmitCIEBasedDEF(LPMEMSTREAM m, LPLUT Lut, int Intent, LPcmsCIEXYZ BlackPoint) { const char* PreMaj; const char* PostMaj; const char* PreMin, *PostMin; switch (Lut ->InputChan) { case 3: Writef(m, "[ /CIEBasedDEF\n"); PreMaj ="<"; PostMaj= ">\n"; PreMin = PostMin = ""; break; case 4: Writef(m, "[ /CIEBasedDEFG\n"); PreMaj = "["; PostMaj = "]\n"; PreMin = "<"; PostMin = ">\n"; break; default: return 0; } Writef(m, "<<\n"); if (Lut ->wFlags & LUT_HASTL1) { Writef(m, "/DecodeDEF [ "); EmitNGamma(m, Lut ->InputChan, Lut ->L1, Lut ->CLut16params.nSamples); Writef(m, "]\n"); } if (Lut ->wFlags & LUT_HAS3DGRID) { Writef(m, "/Table "); WriteCLUT(m, Lut, 8, PreMaj, PostMaj, PreMin, PostMin, TRUE, FALSE, (icColorSpaceSignature) 0); Writef(m, "]\n"); } EmitLab2XYZ(m); EmitWhiteBlackD50(m, BlackPoint); EmitIntent(m, Intent); Writef(m, " >>\n"); Writef(m, "]\n"); return 1; } // Generates a curve from a gray profile static LPGAMMATABLE ExtractGray2Y(cmsHPROFILE hProfile, int Intent) { LPGAMMATABLE Out = cmsAllocGamma(256); cmsHPROFILE hXYZ = cmsCreateXYZProfile(); cmsHTRANSFORM xform = cmsCreateTransform(hProfile, TYPE_GRAY_8, hXYZ, TYPE_XYZ_DBL, Intent, cmsFLAGS_NOTPRECALC); int i; for (i=0; i < 256; i++) { BYTE Gray = (BYTE) i; cmsCIEXYZ XYZ; cmsDoTransform(xform, &Gray, &XYZ, 1); Out ->GammaTable[i] =_cmsClampWord((int) floor(XYZ.Y * 65535.0 + 0.5)); } cmsDeleteTransform(xform); cmsCloseProfile(hXYZ); return Out; } // Because PostScrip has only 8 bits in /Table, we should use // a more perceptually uniform space... I do choose Lab. static int WriteInputLUT(LPMEMSTREAM m, cmsHPROFILE hProfile, int Intent) { cmsHPROFILE hLab; cmsHTRANSFORM xform; icColorSpaceSignature ColorSpace; int nChannels; DWORD InputFormat; int rc; cmsHPROFILE Profiles[2]; cmsCIEXYZ BlackPointAdaptedToD50; // Does create a device-link based transform. // The DeviceLink is next dumped as working CSA. hLab = cmsCreateLabProfile(NULL); ColorSpace = cmsGetColorSpace(hProfile); nChannels = _cmsChannelsOf(ColorSpace); InputFormat = CHANNELS_SH(nChannels) | BYTES_SH(2); cmsDetectBlackPoint(&BlackPointAdaptedToD50, hProfile, Intent,LCMS_BPFLAGS_D50_ADAPTED); // Is a devicelink profile? if (cmsGetDeviceClass(hProfile) == icSigLinkClass) { // if devicelink output already Lab, use it directly if (cmsGetPCS(hProfile) == icSigLabData) { xform = cmsCreateTransform(hProfile, InputFormat, NULL, TYPE_Lab_DBL, Intent, 0); } else { // Nope, adjust output to Lab if possible Profiles[0] = hProfile; Profiles[1] = hLab; xform = cmsCreateMultiprofileTransform(Profiles, 2, InputFormat, TYPE_Lab_DBL, Intent, 0); } } else { // This is a normal profile xform = cmsCreateTransform(hProfile, InputFormat, hLab, TYPE_Lab_DBL, Intent, 0); } if (xform == NULL) { cmsSignalError(LCMS_ERRC_ABORTED, "Cannot create transform Profile -> Lab"); return 0; } // Only 1, 3 and 4 channels are allowed switch (nChannels) { case 1: { LPGAMMATABLE Gray2Y = ExtractGray2Y(hProfile, Intent); EmitCIEBasedA(m, Gray2Y->GammaTable, Gray2Y ->nEntries, &BlackPointAdaptedToD50); cmsFreeGamma(Gray2Y); } break; case 3: case 4: { LPLUT DeviceLink; _LPcmsTRANSFORM v = (_LPcmsTRANSFORM) xform; if (v ->DeviceLink) rc = EmitCIEBasedDEF(m, v->DeviceLink, Intent, &BlackPointAdaptedToD50); else { DeviceLink = _cmsPrecalculateDeviceLink(xform, 0); rc = EmitCIEBasedDEF(m, DeviceLink, Intent, &BlackPointAdaptedToD50); cmsFreeLUT(DeviceLink); } } break; default: cmsSignalError(LCMS_ERRC_ABORTED, "Only 3, 4 channels supported for CSA. This profile has %d channels.", nChannels); return 0; } cmsDeleteTransform(xform); cmsCloseProfile(hLab); return 1; } // Does create CSA based on matrix-shaper. Allowed types are gray and RGB based static int WriteInputMatrixShaper(LPMEMSTREAM m, cmsHPROFILE hProfile) { icColorSpaceSignature ColorSpace; LPMATSHAPER MatShaper; int rc; cmsCIEXYZ BlackPointAdaptedToD50; ColorSpace = cmsGetColorSpace(hProfile); MatShaper = cmsBuildInputMatrixShaper(hProfile); cmsDetectBlackPoint(&BlackPointAdaptedToD50, hProfile, INTENT_RELATIVE_COLORIMETRIC, LCMS_BPFLAGS_D50_ADAPTED); if (MatShaper == NULL) { cmsSignalError(LCMS_ERRC_ABORTED, "This profile is not suitable for input"); return 0; } if (ColorSpace == icSigGrayData) { rc = EmitCIEBasedA(m, MatShaper ->L[0], MatShaper ->p16.nSamples, &BlackPointAdaptedToD50); } else if (ColorSpace == icSigRgbData) { rc = EmitCIEBasedABC(m, MatShaper->L, MatShaper ->p16.nSamples, &MatShaper ->Matrix, &BlackPointAdaptedToD50); } else { cmsSignalError(LCMS_ERRC_ABORTED, "Profile is not suitable for CSA. Unsupported colorspace."); return 0; } cmsFreeMatShaper(MatShaper); return rc; } // 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(LPMEMSTREAM m, cmsHPROFILE hNamedColor, int Intent) { cmsHTRANSFORM xform; cmsHPROFILE hLab; int i, nColors; char ColorName[32]; hLab = cmsCreateLabProfile(NULL); xform = cmsCreateTransform(hNamedColor, TYPE_NAMED_COLOR_INDEX, hLab, TYPE_Lab_DBL, Intent, cmsFLAGS_NOTPRECALC); if (xform == NULL) return 0; Writef(m, "<<\n"); Writef(m, "(colorlistcomment) (%s)\n", "Named color CSA"); Writef(m, "(Prefix) [ (Pantone ) (PANTONE ) ]\n"); Writef(m, "(Suffix) [ ( CV) ( CVC) ( C) ]\n"); nColors = cmsNamedColorCount(xform); for (i=0; i < nColors; i++) { WORD In[1]; cmsCIELab Lab; In[0] = (WORD) i; if (!cmsNamedColorInfo(xform, i, ColorName, NULL, NULL)) continue; cmsDoTransform(xform, In, &Lab, 1); Writef(m, " (%s) [ %.3f %.3f %.3f ]\n", ColorName, Lab.L, Lab.a, Lab.b); } Writef(m, ">>\n"); cmsDeleteTransform(xform); cmsCloseProfile(hLab); return 1; } // Does create a Color Space Array on XYZ colorspace for PostScript usage DWORD LCMSEXPORT cmsGetPostScriptCSA(cmsHPROFILE hProfile, int Intent, LPVOID Buffer, DWORD dwBufferLen) { LPMEMSTREAM mem; DWORD dwBytesUsed; // Set up the serialization engine mem = CreateMemStream((LPBYTE) Buffer, dwBufferLen, MAXPSCOLS); if (!mem) return 0; // Is a named color profile? if (cmsGetDeviceClass(hProfile) == icSigNamedColorClass) { if (!WriteNamedColorCSA(mem, hProfile, Intent)) { _cmsFree((void*) mem); return 0; } } else { // Any profile class are allowed (including devicelink), but // output (PCS) colorspace must be XYZ or Lab icColorSpaceSignature ColorSpace = cmsGetPCS(hProfile); if (ColorSpace != icSigXYZData && ColorSpace != icSigLabData) { cmsSignalError(LCMS_ERRC_ABORTED, "Invalid output color space"); _cmsFree((void*) mem); return 0; } // Is there any CLUT? if (IsLUTbased(hProfile, Intent)) { // Yes, so handle as LUT-based if (!WriteInputLUT(mem, hProfile, Intent)) { _cmsFree((void*) mem); return 0; } } else { // No, try Matrix-shaper (this only works on XYZ) if (!WriteInputMatrixShaper(mem, hProfile)) { _cmsFree((void*) mem); // Something went wrong return 0; } } } // Done, keep memory usage dwBytesUsed = mem ->dwUsed; // Get rid of memory stream _cmsFree((void*) mem); // Finally, return used byte count return dwBytesUsed; } // ------------------------------------------------------ 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(LPMEMSTREAM m, cmsHPROFILE hProfile, int DoBPC, int lIsAbsolute) { if (lIsAbsolute) { // For absolute colorimetric intent, encode back to relative // and generate a relative LUT // Relative encoding is obtained across XYZpcs*(D50/WhitePoint) cmsCIEXYZ White; cmsTakeMediaWhitePoint(&White, hProfile); Writef(m,"/MatrixPQR [1 0 0 0 1 0 0 0 1 ]\n"); Writef(m,"/RangePQR [ -0.5 2 -0.5 2 -0.5 2 ]\n"); Writef(m, "%% Absolute colorimetric -- encode to relative to maximize LUT usage\n" "/TransformPQR [\n" "{0.9642 mul %g div exch pop exch pop exch pop exch pop} bind\n" "{1.0000 mul %g div exch pop exch pop exch pop exch pop} bind\n" "{0.8249 mul %g div exch pop exch pop exch pop exch pop} bind\n]\n", White.X, White.Y, White.Z); return; } Writef(m,"%% Bradford Cone Space\n" "/MatrixPQR [0.8951 -0.7502 0.0389 0.2664 1.7135 -0.0685 -0.1614 0.0367 1.0296 ] \n"); Writef(m, "/RangePQR [ -0.5 2 -0.5 2 -0.5 2 ]\n"); // No BPC if (!DoBPC) { Writef(m, "%% VonKries-like transform in Bradford Cone Space\n" "/TransformPQR [\n" "{exch pop exch 3 get mul exch pop exch 3 get div} bind\n" "{exch pop exch 4 get mul exch pop exch 4 get div} bind\n" "{exch pop exch 5 get mul exch pop exch 5 get div} bind\n]\n"); } else { // BPC Writef(m, "%% VonKries-like transform in Bradford Cone Space plus BPC\n" "/TransformPQR [\n"); Writef(m, "{4 index 3 get div 2 index 3 get mul " "2 index 3 get 2 index 3 get sub mul " "2 index 3 get 4 index 3 get 3 index 3 get sub mul sub " "3 index 3 get 3 index 3 get exch sub div " "exch pop exch pop exch pop exch pop } bind\n"); Writef(m, "{4 index 4 get div 2 index 4 get mul " "2 index 4 get 2 index 4 get sub mul " "2 index 4 get 4 index 4 get 3 index 4 get sub mul sub " "3 index 4 get 3 index 4 get exch sub div " "exch pop exch pop exch pop exch pop } bind\n"); Writef(m, "{4 index 5 get div 2 index 5 get mul " "2 index 5 get 2 index 5 get sub mul " "2 index 5 get 4 index 5 get 3 index 5 get sub mul sub " "3 index 5 get 3 index 5 get exch sub div " "exch pop exch pop exch pop exch pop } bind\n]\n"); } } static void EmitXYZ2Lab(LPMEMSTREAM m) { Writef(m, "/RangeLMN [ -0.635 2.0 0 2 -0.635 2.0 ]\n"); Writef(m, "/EncodeLMN [\n"); Writef(m, "{ 0.964200 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind\n"); Writef(m, "{ 1.000000 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind\n"); Writef(m, "{ 0.824900 div dup 0.008856 le {7.787 mul 16 116 div add}{1 3 div exp} ifelse } bind\n"); Writef(m, "]\n"); Writef(m, "/MatrixABC [ 0 1 0 1 -1 1 0 0 -1 ]\n"); Writef(m, "/EncodeABC [\n"); Writef(m, "{ 116 mul 16 sub 100 div } bind\n"); Writef(m, "{ 500 mul 128 add 256 div } bind\n"); Writef(m, "{ 200 mul 128 add 256 div } bind\n"); Writef(m, "]\n"); } // 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 accurancy. Note also that CRD tables must operate in // 8 bits. static int WriteOutputLUT(LPMEMSTREAM m, cmsHPROFILE hProfile, int Intent, DWORD dwFlags) { cmsHPROFILE hLab; cmsHTRANSFORM xform; icColorSpaceSignature ColorSpace; int i, nChannels; DWORD OutputFormat; _LPcmsTRANSFORM v; LPLUT DeviceLink; cmsHPROFILE Profiles[3]; cmsCIEXYZ BlackPointAdaptedToD50; LCMSBOOL lFreeDeviceLink = FALSE; LCMSBOOL lDoBPC = (dwFlags & cmsFLAGS_BLACKPOINTCOMPENSATION); LCMSBOOL lFixWhite = !(dwFlags & cmsFLAGS_NOWHITEONWHITEFIXUP); int RelativeEncodingIntent; hLab = cmsCreateLabProfile(NULL); ColorSpace = cmsGetColorSpace(hProfile); nChannels = _cmsChannelsOf(ColorSpace); OutputFormat = CHANNELS_SH(nChannels) | BYTES_SH(2); // For absolute colorimetric, the LUT is encoded as relative // in order to preserve precission. RelativeEncodingIntent = Intent; if (RelativeEncodingIntent == INTENT_ABSOLUTE_COLORIMETRIC) RelativeEncodingIntent = INTENT_RELATIVE_COLORIMETRIC; // Is a devicelink profile? if (cmsGetDeviceClass(hProfile) == icSigLinkClass) { // if devicelink input already in Lab if (ColorSpace == icSigLabData) { // adjust input to Lab to our v4 Profiles[0] = hLab; Profiles[1] = hProfile; xform = cmsCreateMultiprofileTransform(Profiles, 2, TYPE_Lab_DBL, OutputFormat, RelativeEncodingIntent, dwFlags|cmsFLAGS_NOWHITEONWHITEFIXUP|cmsFLAGS_NOPRELINEARIZATION); } else { cmsSignalError(LCMS_ERRC_ABORTED, "Cannot use devicelink profile for CRD creation"); return 0; } } else { // This is a normal profile xform = cmsCreateTransform(hLab, TYPE_Lab_DBL, hProfile, OutputFormat, RelativeEncodingIntent, dwFlags|cmsFLAGS_NOWHITEONWHITEFIXUP|cmsFLAGS_NOPRELINEARIZATION); } if (xform == NULL) { cmsSignalError(LCMS_ERRC_ABORTED, "Cannot create transform Lab -> Profile in CRD creation"); return 0; } // Get the internal precalculated devicelink v = (_LPcmsTRANSFORM) xform; DeviceLink = v ->DeviceLink; if (!DeviceLink) { DeviceLink = _cmsPrecalculateDeviceLink(xform, cmsFLAGS_NOPRELINEARIZATION); lFreeDeviceLink = TRUE; } Writef(m, "<<\n"); Writef(m, "/ColorRenderingType 1\n"); cmsDetectBlackPoint(&BlackPointAdaptedToD50, hProfile, Intent, LCMS_BPFLAGS_D50_ADAPTED); // Emit headers, etc. EmitWhiteBlackD50(m, &BlackPointAdaptedToD50); EmitPQRStage(m, hProfile, lDoBPC, Intent == INTENT_ABSOLUTE_COLORIMETRIC); EmitXYZ2Lab(m); if (DeviceLink ->wFlags & LUT_HASTL1) { // Shouldn't happen cmsSignalError(LCMS_ERRC_ABORTED, "Internal error (prelinearization on CRD)"); return 0; } // FIXUP: map Lab (100, 0, 0) to perfect white, because the particular encoding for Lab // does map a=b=0 not falling into any specific node. Since range a,b goes -128..127, // zero is slightly moved towards right, so assure next node (in L=100 slice) is mapped to // zero. This would sacrifice a bit of highlights, but failure to do so would cause // scum dot. Ouch. if (Intent == INTENT_ABSOLUTE_COLORIMETRIC) lFixWhite = FALSE; Writef(m, "/RenderTable "); WriteCLUT(m, DeviceLink, 8, "<", ">\n", "", "", FALSE, lFixWhite, ColorSpace); Writef(m, " %d {} bind ", nChannels); for (i=1; i < nChannels; i++) Writef(m, "dup "); Writef(m, "]\n"); EmitIntent(m, Intent); Writef(m, ">>\n"); if (!(dwFlags & cmsFLAGS_NODEFAULTRESOURCEDEF)) { Writef(m, "/Current exch /ColorRendering defineresource pop\n"); } if (lFreeDeviceLink) cmsFreeLUT(DeviceLink); cmsDeleteTransform(xform); cmsCloseProfile(hLab); return 1; } // Builds a ASCII string containing colorant list in 0..1.0 range static void BuildColorantList(char *Colorant, int nColorant, WORD Out[]) { char Buff[32]; int j; Colorant[0] = 0; if (nColorant > MAXCHANNELS) nColorant = MAXCHANNELS; for (j=0; j < nColorant; j++) { sprintf(Buff, "%.3f", Out[j] / 65535.0); strcat(Colorant, Buff); if (j < nColorant -1) strcat(Colorant, " "); } } // Creates a PostScript color list from a named profile data. // This is a HP extension. static int WriteNamedColorCRD(LPMEMSTREAM m, cmsHPROFILE hNamedColor, int Intent, DWORD dwFlags) { cmsHTRANSFORM xform; int i, nColors, nColorant; DWORD OutputFormat; char ColorName[32]; char Colorant[128]; nColorant = _cmsChannelsOf(cmsGetColorSpace(hNamedColor)); OutputFormat = CHANNELS_SH(nColorant) | BYTES_SH(2); xform = cmsCreateTransform(hNamedColor, TYPE_NAMED_COLOR_INDEX, NULL, OutputFormat, Intent, cmsFLAGS_NOTPRECALC); if (xform == NULL) return 0; Writef(m, "<<\n"); Writef(m, "(colorlistcomment) (%s) \n", "Named profile"); Writef(m, "(Prefix) [ (Pantone ) (PANTONE ) ]\n"); Writef(m, "(Suffix) [ ( CV) ( CVC) ( C) ]\n"); nColors = cmsNamedColorCount(xform); for (i=0; i < nColors; i++) { WORD In[1]; WORD Out[MAXCHANNELS]; In[0] = (WORD) i; if (!cmsNamedColorInfo(xform, i, ColorName, NULL, NULL)) continue; cmsDoTransform(xform, In, Out, 1); BuildColorantList(Colorant, nColorant, Out); Writef(m, " (%s) [ %s ]\n", ColorName, Colorant); } Writef(m, " >>"); if (!(dwFlags & cmsFLAGS_NODEFAULTRESOURCEDEF)) { Writef(m, " /Current exch /HPSpotTable defineresource pop\n"); } cmsDeleteTransform(xform); return 1; } // This one does create a Color Rendering Dictionary. // CRD are always LUT-Based, no matter if profile is // implemented as matrix-shaper. DWORD LCMSEXPORT cmsGetPostScriptCRDEx(cmsHPROFILE hProfile, int Intent, DWORD dwFlags, LPVOID Buffer, DWORD dwBufferLen) { LPMEMSTREAM mem; DWORD dwBytesUsed; // Set up the serialization artifact mem = CreateMemStream((LPBYTE) Buffer, dwBufferLen, MAXPSCOLS); if (!mem) return 0; if (!(dwFlags & cmsFLAGS_NODEFAULTRESOURCEDEF)) { EmitHeader(mem, "Color Rendering Dictionary (CRD)", hProfile); } // Is a named color profile? if (cmsGetDeviceClass(hProfile) == icSigNamedColorClass) { if (!WriteNamedColorCRD(mem, hProfile, Intent, dwFlags)) { _cmsFree((void*) mem); return 0; } } else { // CRD are always implemented as LUT. if (!WriteOutputLUT(mem, hProfile, Intent, dwFlags)) { _cmsFree((void*) mem); return 0; } } if (!(dwFlags & cmsFLAGS_NODEFAULTRESOURCEDEF)) { Writef(mem, "%%%%EndResource\n"); Writef(mem, "\n%% CRD End\n"); } // Done, keep memory usage dwBytesUsed = mem ->dwUsed; // Get rid of memory stream _cmsFree((void*) mem); // Finally, return used byte count return dwBytesUsed; } // For compatibility with previous versions DWORD LCMSEXPORT cmsGetPostScriptCRD(cmsHPROFILE hProfile, int Intent, LPVOID Buffer, DWORD dwBufferLen) { return cmsGetPostScriptCRDEx(hProfile, Intent, 0, Buffer, dwBufferLen); }