1 //------------------------------------------------------------------------------
  2 // File: jamBooks.jsxinc
  3 // Version: 4.5.1
  4 // Release Date: 2016-10-13
  5 // Copyright: © 2016 Michel MARIANI <http://www.tonton-pixel.com/blog/>
  6 // Licence: GPL <http://www.gnu.org/licenses/gpl.html>
  7 //------------------------------------------------------------------------------
  8 // This program is free software: you can redistribute it and/or modify
  9 // it under the terms of the GNU General Public License as published by
 10 // the Free Software Foundation, either version 3 of the License, or
 11 // (at your option) any later version.
 12 // 
 13 // This program is distributed in the hope that it will be useful,
 14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 // GNU General Public License for more details.
 17 // 
 18 // You should have received a copy of the GNU General Public License
 19 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 20 //------------------------------------------------------------------------------
 21 // Version History:
 22 //  4.5.1:
 23 //  - Added book description and simplified name of color components fields in 
 24 //    jamBooks.getColorBookFileColors ().
 25 //  4.5:
 26 //  - Initial release.
 27 //------------------------------------------------------------------------------
 28 
 29 /**
 30  * @fileOverview
 31  * @name jamBooks.jsxinc
 32  * @author Michel MARIANI
 33  */
 34 
 35 //------------------------------------------------------------------------------
 36 
 37 if (typeof jamBooks !== 'object')
 38 {
 39     /**
 40      * Global object (used to simulate a namespace in JavaScript) containing
 41      * a set of color books-related functions for scripts written with the
 42      * <a href="http://www.tonton-pixel.com/blog/json-photoshop-scripting/json-action-manager/">JSON Action Manager</a> engine.<br>
 43      * Uses information found in the documents
 44      * <a href="http://magnetiq.com/pages/acb-spec/">Adobe Color Book File Format Specification</a> and
 45      * <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1066780">Adobe Photoshop File Formats Specification</a>.
 46      * @author Michel MARIANI
 47      * @version 4.5.1
 48      * @namespace
 49      */
 50     var jamBooks = { };
 51     //
 52     (function ()
 53     {
 54         /**
 55          * @description Test if a given file is a color book file (*.acb).
 56          * @param {Object} file File object
 57          * @returns {Boolean} true if color book file
 58          * @example
 59          * function colorBookFileFilter (f)
 60          * {
 61          *     return (f instanceof Folder) || jamBooks.<strong>isColorBookFile</strong> (f);
 62          * }
 63          * var select = (File.fs === "Macintosh") ? colorBookFileFilter : "Color Book Files:*.acb,All Files:*";
 64          * var colorBookFile = File.openDialog ("Select a color book file:", select);
 65          * if (colorBookFile !== null)
 66          * {
 67          *     alert ("OK!");
 68          * }
 69          */
 70         jamBooks.isColorBookFile = function (file)
 71         {
 72             return (file.type === '8BCB') || file.name.match (/\.acb$/i);
 73         };
 74         //
 75         /**
 76          * @description Parse a color book file (*.acb) into a data structure in JSON object format.
 77          * @param {String|Object} colorBookFile Color book file path string or File object
 78          * @param {Boolean} [actualComponents] List actual color components instead of raw color components
 79          * @returns {Object|String} Parsed color book file data structure in JSON object format, or error message string
 80          * @see jamBooks.dataToColorBookFile
 81          * @example
 82          * var acbFilter =
 83          *     (File.fs === "Macintosh") ?
 84          *         function (f) { return (f instanceof Folder) || jamBooks.isColorBookFile (f) } :
 85          *         "Color Book Files:*.acb,All Files:*.*";
 86          * var colorBookFile = File.openDialog ("Open color book file:", acbFilter);
 87          * if (colorBookFile)
 88          * {
 89          *     var colorBookData = jamBooks.<strong>dataFromColorBookFile</strong> (colorBookFile);
 90          *     if (typeof colorBookData === 'string')
 91          *     {
 92          *         alert ("Error: " + colorBookData);
 93          *     }
 94          *     else
 95          *     {
 96          *         alert ("Color space: " + colorBookData["colorSpace"]);
 97          *         alert ("Number of colors: " + colorBookData["colors"].length);
 98          *     }
 99          * }
100          */
101         jamBooks.dataFromColorBookFile = function (colorBookFile, actualComponents)
102         {
103             var file;
104             if (typeof colorBookFile === 'string')
105             {
106                 file = new File (colorBookFile);
107             }
108             else if (colorBookFile instanceof File)
109             {
110                 file = colorBookFile;
111             }
112             //
113             var colorBookData;
114             if (file.open ("r"))
115             {
116                 try
117                 {
118                     file.encoding = 'BINARY';
119                     var magicNumber = file.read (4);
120                     if (magicNumber === '8BCB')
121                     {
122                         function readBEInt (file, byteCount)
123                         {
124                             var bytes = file.read (byteCount);
125                             var intValue = 0;
126                             for (var index = 0; index < byteCount; index++)
127                             {
128                                 intValue = (intValue << 8) + bytes.charCodeAt (index);
129                             }
130                             return intValue;
131                         }
132                         function readUnicodeString (file)
133                         {
134                             var unicodeString = "";
135                             var unicodeLength = readBEInt (file, 4);    // Includes terminating null
136                             for (var index = 0; index < unicodeLength; index++)
137                             {
138                                 var unicodeChar = readBEInt (file, 2);
139                                 if (unicodeChar !== 0)
140                                 {
141                                     unicodeString += String.fromCharCode (unicodeChar);
142                                 }
143                             }
144                             return unicodeString;
145                         }
146                         var formatVersion = readBEInt (file, 2);
147                         if (formatVersion === 1)
148                         {
149                             var colorBook = { };
150                             colorBook["bookID"] = readBEInt (file, 2);
151                             colorBook["bookName"] = readUnicodeString (file);
152                             colorBook["colorNamePrefix"] = readUnicodeString (file);
153                             colorBook["colorNameSuffix"] = readUnicodeString (file);
154                             colorBook["bookDescription"] = readUnicodeString (file);
155                             var colorCount = readBEInt (file, 2);
156                             colorBook["colorsPerPage"] = readBEInt (file, 2);
157                             colorBook["keyColorIndex"] = readBEInt (file, 2);
158                             var colorSpace = readBEInt (file, 2);
159                             var colorSpaces = [ "RGB", null, "CMYK", null, null, null, null, "Lab" ];
160                             if ((colorSpace < 0) || (colorSpace > colorSpaces.length))
161                             {
162                                  throw new Error ("[jamBooks.dataFromColorBookFile] Invalid color space: " + colorSpace);
163                             }
164                             else
165                             {
166                                 colorBook["colorSpace"] = colorSpaces[colorSpace];
167                             }
168                             var colors = [ ];
169                             for (var colorIndex = 0; colorIndex < colorCount; colorIndex++)
170                             {
171                                 var color = { };
172                                 color["colorName"] = readUnicodeString (file);
173                                 color["colorKey"] = file.read (6);
174                                 var components = file.read ((colorSpace === 2) ? 4 : 3);
175                                 var componentArr = [ ];
176                                 for (componentIndex =  0; componentIndex < components.length; componentIndex++)
177                                 {
178                                     var componentValue = components.charCodeAt (componentIndex);
179                                     if (actualComponents)
180                                     {
181                                         switch (colorSpace)
182                                         {
183                                             case 2: // CMYK
184                                                 componentValue = (255 - componentValue) / 255 * 100; // 0% to 100%
185                                                 break;
186                                             case 7: // Lab
187                                                 if (componentIndex > 0) // a or b
188                                                 {
189                                                     componentValue -= 128;  // -128 to 127
190                                                 }
191                                                 else    // L (luminance)
192                                                 {
193                                                     componentValue = componentValue / 255 * 100;    // 0% to 100%
194                                                 }
195                                                 break;
196                                         }
197                                     }
198                                     componentArr.push (componentValue);
199                                 }
200                                 color[(actualComponents) ? "actualComponents" : "rawComponents"] = componentArr;
201                                 colors.push (color);
202                             }
203                             colorBook["colors"] = colors;
204                             if (!file.eof)
205                             {
206                                 colorBook["spotProcess"] = file.read (8);   // Either 'spflspot' or 'spflproc'
207                             }
208                             colorBookData = colorBook;
209                         }
210                         else
211                         {
212                             throw new Error ("[jamBooks.dataFromColorBookFile] Unrecognized format version: " + formatVersion);
213                         }
214                     }
215                     else
216                     {
217                         throw new Error ("[jamBooks.dataFromColorBookFile] Unrecognized magic number: " + magicNumber);
218                     }
219                 }
220                 catch (e)
221                 {
222                     colorBookData = e.message;
223                 }
224                 finally
225                 {
226                     file.close ();
227                 }
228             }
229             else
230             {
231                 colorBookData = "[jamBooks.dataFromColorBookFile] Cannot open file";
232             }
233             return colorBookData;
234         };
235         //
236         /**
237          * @description Generate a color book file (*.acb) from a data structure in JSON object format.
238          * @param {String|Object} colorBookFile Color book file path string or File object
239          * @param {Object} colorBookData Color book file data structure in JSON object format
240          * @returns {String} Error message string (empty if no error)
241          * @see jamBooks.dataFromColorBookFile
242          * @example
243          * var jsonFilter =
244          *     (File.fs === "Macintosh") ?
245          *         function (f) { return (f instanceof Folder) || f.name.match (/\.json$/i) } :
246          *         "JSON Text Files:*.json,All Files:*.*";
247          * var jsonFile = File.openDialog ("Open JSON text file:", jsonFilter);
248          * if (jsonFile)
249          * {
250          *     var colorBookData = jamUtils.readJsonFile (jsonFile);
251          *     if (colorBookData)
252          *     {
253          *         var result = jamBooks.<strong>dataToColorBookFile</strong> ("~/Desktop/color-book.acb", colorBookData);
254          *         if (result)
255          *         {
256          *             alert ("Error: " + result);
257          *         }
258          *         else
259          *         {
260          *             alert ("Color book file generated on Desktop.");
261          *         }
262          *     }
263          *     else
264          *     {
265          *         alert ("Invalid JSON text file!");
266          *     }
267          * }
268          */
269         jamBooks.dataToColorBookFile = function (colorBookFile, colorBookData)
270         {
271             var file;
272             if (typeof colorBookFile === 'string')
273             {
274                 file = new File (colorBookFile);
275             }
276             else if (colorBookFile instanceof File)
277             {
278                 file = colorBookFile;
279             }
280             //
281             var result = "";
282             if (file.open ('w', '8BCB', '8BIM'))
283             {
284                 try
285                 {
286                     function writeBEInt (file, byteCount, intValue)
287                     {
288                         var bytes = "";
289                         for (var index = 0; index < byteCount; index++)
290                         {
291                             bytes = String.fromCharCode (intValue & 0xFF) + bytes;
292                             intValue >>= 8;
293                         }
294                         file.write (bytes);
295                     }
296                     function writeUnicodeString (file, unicodeString)
297                     {
298                         var unicodeLength = unicodeString.length;
299                         writeBEInt (file, 4, unicodeLength);    // Doesn't include terminating null!
300                         for (var index = 0; index < unicodeLength; index++)
301                         {
302                             writeBEInt (file, 2, unicodeString.charCodeAt (index));
303                         }
304                     }
305                     file.encoding = "BINARY";
306                     file.write ('8BCB');
307                     writeBEInt (file, 2, 1);
308                     writeBEInt (file, 2, colorBookData["bookID"]);
309                     writeUnicodeString (file, colorBookData["bookName"]);
310                     writeUnicodeString (file, colorBookData["colorNamePrefix"]);
311                     writeUnicodeString (file, colorBookData["colorNameSuffix"]);
312                     writeUnicodeString (file, colorBookData["bookDescription"]);
313                     var colors = colorBookData["colors"];
314                     var colorCount = colors.length;
315                     writeBEInt (file, 2, colorCount);
316                     writeBEInt (file, 2, colorBookData["colorsPerPage"]);
317                     if ("keyColorIndex" in colorBookData)
318                     {
319                         writeBEInt (file, 2, colorBookData["keyColorIndex"]);
320                     }
321                     else if ("keyColorPage" in colorBookData)  // Legacy...
322                     {
323                         writeBEInt (file, 2, colorBookData["keyColorPage"]);
324                     }
325                     var colorSpace = colorBookData["colorSpace"];
326                     var colorSpaces = { "RGB": 0, "CMYK": 2, "Lab": 7 };
327                     if (colorSpace in colorSpaces)
328                     {
329                         writeBEInt (file, 2, colorSpaces[colorSpace]);
330                     }
331                     else
332                     {
333                         throw new Error ("[jamBooks.dataToColorBookFile] Invalid color space: " + jamJSON.stringify (colorSpace));
334                     }
335                     for (var colorIndex = 0; colorIndex < colorCount; colorIndex++)
336                     {
337                         var color = colors[colorIndex];
338                         writeUnicodeString (file, color["colorName"]);
339                         var colorKey = color["colorKey"];
340                         if (colorKey.length === 6)
341                         {
342                             file.write (colorKey);
343                         }
344                         else
345                         {
346                             throw new Error ("[jamBooks.dataToColorBookFile] Invalid color key: " + jamJSON.stringify (colorKey));
347                         }
348                         if ("actualComponents" in color)
349                         {
350                             var components = color["actualComponents"];
351                             for (componentIndex =  0; componentIndex < components.length; componentIndex++)
352                             {
353                                 var componentValue = components[componentIndex];
354                                 switch (colorSpace)
355                                 {
356                                     case "CMYK":
357                                         componentValue = 255 - Math.round (componentValue * 255 / 100);
358                                         break;
359                                     case "Lab":
360                                         if (componentIndex > 0) // a or b
361                                         {
362                                             componentValue = Math.round (componentValue) + 128;
363                                         }
364                                         else    // L (luminance)
365                                         {
366                                             componentValue = Math.round (componentValue * 255 / 100);
367                                         }
368                                         break;
369                                     case "RGB":
370                                         componentValue = Math.round (componentValue);
371                                         break;
372                                 }
373                                 file.write (String.fromCharCode (componentValue));
374                             }
375                         }
376                         else if ("rawComponents" in color)
377                         {
378                             var components = color["rawComponents"];
379                             for (componentIndex =  0; componentIndex < components.length; componentIndex++)
380                             {
381                                 file.write (String.fromCharCode (components[componentIndex]));
382                             }
383                         }
384                         else if ("components" in color) // Legacy...
385                         {
386                             var components = color["components"];
387                             for (componentIndex =  0; componentIndex < components.length; componentIndex++)
388                             {
389                                 file.write (String.fromCharCode (components[componentIndex]));
390                             }
391                         }
392                     }
393                     if ("spotProcess" in colorBookData)
394                     {
395                         file.write (colorBookData["spotProcess"]);  // Either 'spflspot' or 'spflproc'
396                     }
397                 }
398                 catch (e)
399                 {
400                     result = e.message;
401                 }
402                 finally
403                 {
404                     file.close ();
405                 }
406             }
407             else
408             {
409                 result = "[jamBooks.dataToColorBookFile] Cannot open file";
410             }
411             return result;
412         };
413         //
414         /**
415          * @description Get the list of colors from a color book file (*.acb).
416          * @param {String|Object} colorBookFile Color book file path string or File object
417          * @param {Boolean} [roundComponents] Round color components
418          * @returns {Object|Null} List of colors from a color book file (*.acb), or null if error
419          * @example
420          * var acbFilter =
421          *     (File.fs === "Macintosh") ?
422          *         function (f) { return (f instanceof Folder) || jamBooks.isColorBookFile (f) } :
423          *         "Color Book Files:*.acb,All Files:*.*";
424          * var colorBookFile = File.openDialog ("Open color book file:", acbFilter);
425          * if (colorBookFile )
426          * {
427          *     var colorBookColors = jamBooks.<strong>getColorBookFileColors</strong> (colorBookFile, true);
428          *     if (colorBookColors)
429          *     {
430          *         alert ("First color:\r"+ jamJSON.stringify (colorBookColors["bookColors"][0], '\t'));
431          *     }
432          * }
433          */
434         jamBooks.getColorBookFileColors = function (colorBookFile, roundComponents)
435         {
436             var colorBookColors = null;
437             var colorBookData = this.dataFromColorBookFile (colorBookFile, true);
438             if (colorBookData !== 'string')
439             {
440                 colorBookColors = { };
441                 colorBookColors["bookFile"] = File.decode (colorBookFile.name);
442                 colorBookColors["bookName"] = localize (colorBookData["bookName"]);
443                 colorBookColors["bookDescription"] = localize (colorBookData["bookDescription"]);
444                 var colorNamePrefix = localize (colorBookData["colorNamePrefix"]);
445                 var colorNameSuffix = localize (colorBookData["colorNameSuffix"]);
446                 var colorSpace = colorBookData["colorSpace"];
447                 var colors = colorBookData["colors"];
448                 bookColors = [ ];
449                 for (var i = 0; i < colors.length; i++)
450                 {
451                     var bookColor = { };
452                     var colorName = localize (colors[i]["colorName"]);
453                     if (colorName)
454                     {
455                         bookColor["name"] = colorNamePrefix + colorName + colorNameSuffix;
456                         var components = colors[i]["actualComponents"];
457                         var color = { };
458                         switch (colorSpace)
459                         {
460                             case "CMYK":
461                                 color["C"] = (roundComponents) ? Math.round (components[0]) : components[0];
462                                 color["M"] = (roundComponents) ? Math.round (components[1]) : components[1];
463                                 color["Y"] = (roundComponents) ? Math.round (components[2]) : components[2];
464                                 color["K"] = (roundComponents) ? Math.round (components[3]) : components[3];
465                                 break;
466                             case "Lab":
467                                 color["L"] = (roundComponents) ? Math.round (components[0]) : components[0];
468                                 color["a"] = components[1];
469                                 color["b"] = components[2];
470                                 break;
471                             case "RGB":
472                                 color["R"] = components[0];
473                                 color["G"] = components[1];
474                                 color["B"] = components[2];
475                                 break;
476                         }
477                         bookColor["color"] = color;
478                         bookColors.push (bookColor);
479                     }
480                 }
481                 colorBookColors["bookColors"] = bookColors;
482             }
483             return colorBookColors;
484         };
485     } ());
486 }
487 
488 //------------------------------------------------------------------------------
489 
490