1 //------------------------------------------------------------------------------
  2 // File: jamShapes.jsxinc
  3 // Version: 4.5
  4 // Release Date: 2016-09-29
  5 // Copyright: © 2011-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:
 23 //  - Incremented version number to keep in sync with other modules.
 24 //  4.4:
 25 //  - Normalized error messages.
 26 //  4.1:
 27 //  - Simplified test in jamShapes.isCustomShapesPrefsFile ().
 28 //  4.0:
 29 //  - Removed reference to 'this' for main global object.
 30 //  3.6:
 31 //  - Incremented version number to keep in sync with other modules.
 32 //  3.5:
 33 //  - Renamed field "id" to "ID" in data returned by dataFromCustomShapesFile ().
 34 //  3.4.3:
 35 //  - Added parameter shapeIndex to dataFromCustomShapesFile ().
 36 //  - Renamed field "uuid" to "id" in data returned by dataFromCustomShapesFile ().
 37 //  3.4.2:
 38 //  - Added global option: jamShapes.debugMode.
 39 //  3.4.1:
 40 //  - Cleaned up some code.
 41 //  3.4:
 42 //  - Initial release.
 43 //------------------------------------------------------------------------------
 44 
 45 /**
 46  * @fileOverview
 47  * @name jamShapes.jsxinc
 48  * @author Michel MARIANI
 49  */
 50 
 51 //------------------------------------------------------------------------------
 52 
 53 if (typeof jamShapes !== 'object')
 54 {
 55     /**
 56      * Global object (used to simulate a namespace in JavaScript) containing
 57      * a set of functions related to decoding custom shapes files into a format usable by scripts written with the
 58      * <a href="http://www.tonton-pixel.com/blog/json-photoshop-scripting/json-action-manager/">JSON Action Manager</a> engine.<br />
 59      * Uses information found in the document
 60      * <a href="http://www.tonton-pixel.com/Photoshop%20Additional%20File%20Formats/custom-shapes-file-format.html">Photoshop Custom Shapes File Format</a>.
 61      * @author Michel MARIANI
 62      * @version 4.5
 63      * @namespace
 64      */
 65     var jamShapes = { };
 66     //
 67     (function ()
 68     {
 69         /**
 70          * @description Test if a given file is a custom shapes file (*.csh).
 71          * @param {Object} file File object
 72          * @returns {Boolean} true if custom shapes file
 73          * @example
 74          * function customShapesFileFilter (f)
 75          * {
 76          *     return (f instanceof Folder) || jamShapes.<strong>isCustomShapesFile</strong> (f);
 77          * }
 78          * var select = (File.fs === "Macintosh") ? customShapesFileFilter : "Custom Shapes Files:*.csh,All Files:*";
 79          * var customShapesFile = File.openDialog ("Select a custom shapes file:", select);
 80          * if (customShapesFile !== null)
 81          * {
 82          *     alert ("OK!");
 83          * }
 84          */
 85         jamShapes.isCustomShapesFile = function (file)
 86         {
 87             return (file.type === '8BCS') || file.name.match (/\.csh$/i);
 88         };
 89         //
 90         /**
 91          * @description Test if a given file is a custom shapes preferences file (CustomShapes.psp).
 92          * @param {Object} file File object
 93          * @returns {Boolean} true if custom shapes preferences file
 94          * @example
 95          * function customShapesPrefsFileFilter (f)
 96          * {
 97          *     return (f instanceof Folder) || jamShapes.<strong>isCustomShapesPrefsFile</strong> (f);
 98          * }
 99          * var select = (File.fs === "Macintosh") ?
100          *                  customShapesPrefsFileFilter :
101          *                  "Custom Shapes Preferences File:CustomShapes.psp,All Files:*.*";
102          * var customShapesPrefsFile = File.openDialog ("Select a custom shapes preferences file:", select);
103          * if (customShapesPrefsFile !== null)
104          * {
105          *     alert ("OK!");
106          * }
107          */
108         jamShapes.isCustomShapesPrefsFile = function (file)
109         {
110             return file.name.match (/^CustomShapes.psp$/i);
111         };
112         //
113         /**
114          * @description Convert a custom shapes file (*.csh or CustomShapes.psp) into a data structure in JSON format.
115          * @param {String|Object} shapesFile Custom shapes file path string or File object
116          * @param {Number} [shapeIndex] If defined, the returned information for each custom shape is limited to its name and ID 
117          * (no bounds, no path records) except for the shape located at this index; passing -1 will limit the information for 
118          * all custom shapes
119          * @returns {Object|String} Converted custom shapes file data structure in JSON format, or error message string
120          * <p style="margin: 0.5em 0 0 0em;">
121          * The custom shapes file data structure is defined as a JSON object { } with two members:<br />
122          * <code>{ "fileVersion": <em>fileVersion</em>, "customShapes": <em>customShapes</em> }</code>
123          * </p>
124          * <p style="margin: 0.5em 0 0 1.25em;">
125          * <code><em>fileVersion</em></code>: number<br />
126          * <code><em>customShapes</em></code>: JSON array [ ] of <code><em>customShape</em></code>
127          * </p>
128          * <p style="margin: 0.5em 0 0 2.5em;">
129          * <code><em>customShape</em></code>: JSON object { } with four members:<br />
130          * <code>{ "name": <em>name</em>, "ID": <em>ID</em>, "bounds": <em>bounds</em>, 
131          * "pathRecords": <em>pathRecords</em> }</code>
132          * </p>
133          * <p style="margin: 0.5em 0 0 3.75em;">
134          * <code><em>name</em></code>: string<br />
135          * <code><em>ID</em></code>: string<br />
136          * <code><em>bounds</em></code>: JSON array [ ] with four items: 
137          * <code>[ <em>top</em>, <em>left</em>, <em>bottom</em>, <em>right</em> ]</code>
138          * </p>
139          * <p style="margin: 0.5em 0 0 5em;">
140          * <code><em>top</em></code>: number<br />
141          * <code><em>left</em></code>: number<br />
142          * <code><em>bottom</em></code>: number<br />
143          * <code><em>right</em></code>: number<br />
144          * </p>
145          * <p style="margin: 0.5em 0 0 3.75em;">
146          * <code><em>pathRecords</em></code>: JSON array [ ] of <code><em>pathRecord</em></code>
147          * </p>
148          * <p style="margin: 0.5em 0 0 5em;">
149          * <code><em>pathRecord</em></code>: JSON array [ ] with two items: <code>[ <em>selector</em>, <em>data</em> }</code>
150          * </p>
151          * <table style="margin: 0.5em 0 0 6.25em; border-collapse: collapse;">
152          * <tr style="border: 1px solid #E7E7E7;">
153          * <th style="text-align: left; color: gray; padding: 0.25em 2em;"><code><em>selector</em></code></th>
154          * <th style="text-align: left; color: gray; padding: 0.25em 2em;"><code><em>data</em></code></th>
155          * </tr>
156          * <tr style="border: 1px solid #E7E7E7;">
157          * <td style="padding: 0.25em 2em;"><code>"pathFill"</code></td>
158          * <td style="padding: 0.25em 2em;"><code>null</null></td>
159          * </tr>
160          * <tr style="border: 1px solid #E7E7E7;">
161          * <td style="padding: 0.25em 2em;"><code>"initialFill"</code></td>
162          * <td style="padding: 0.25em 2em;">number (0 or 1)</code></td>
163          * </tr>
164          * <tr style="border: 1px solid #E7E7E7;">
165          * <td style="padding: 0.25em 2em;"><code>"closedLength"</code></td>
166          * <td style="padding: 0.25em 2em;">number</td>
167          * </tr>
168          * <tr style="border: 1px solid #E7E7E7;">
169          * <td style="padding: 0.25em 2em;"><code>"closedLinked"</code></td>
170          * <td style="padding: 0.25em 2em;">JSON array [ ] with three items: <code>[ <em>backward</em>, <em>anchor</em>, <em>forward</em> ]</code></td>
171          * </tr>
172          * <tr style="border: 1px solid #E7E7E7;">
173          * <td style="padding: 0.25em 2em;"><code>"closedUnlinked"</code></td>
174          * <td style="padding: 0.25em 2em;">JSON array [ ] with three items: <code>[ <em>backward</em>, <em>anchor</em>, <em>forward</em> ]</code></td>
175          * </tr>
176          * <tr style="border: 1px solid #E7E7E7;">
177          * <td style="padding: 0.25em 2em;"><code>"openLength"</code></td>
178          * <td style="padding: 0.25em 2em;">number</td>
179          * </tr>
180          * <tr style="border: 1px solid #E7E7E7;">
181          * <td style="padding: 0.25em 2em;"><code>"openLinked"</code></td>
182          * <td style="padding: 0.25em 2em;">JSON array [ ] with three items: <code>[ <em>backward</em>, <em>anchor</em>, <em>forward</em> ]</code></td>
183          * </tr>
184          * <tr style="border: 1px solid #E7E7E7;">
185          * <td style="padding: 0.25em 2em;"><code>"openUnlinked"</code></td>
186          * <td style="padding: 0.25em 2em;">JSON array [ ] with three items: <code>[ <em>backward</em>, <em>anchor</em>, <em>forward</em> ]</code></td>
187          * </tr>
188          * </table>
189          * <p style="margin: 0.5em 0 0 7.5em;">
190          * <code><em>backward</em></code>: JSON array [ ] with two items: <code>[ <em>vertical</em>, <em>horizontal</em> ]</code><br />
191          * <code><em>anchor</em></code>: JSON array [ ] with two items: <code>[ <em>vertical</em>, <em>horizontal</em> ]</code><br />
192          * <code><em>forward</em></code>: JSON array [ ] with two items: <code>[ <em>vertical</em>, <em>horizontal</em> ]</code><br />
193          * </p>
194          * <p style="margin: 0.5em 0 0 8.75em;">
195          * <code><em>vertical</em></code>: number<br />
196          * <code><em>horizontal</em></code>: number<br />
197          * </p>
198          * @example
199          * function customShapesFileFilter (f)
200          * {
201          *     return (f instanceof Folder) || jamShapes.isCustomShapesFile (f);
202          * }
203          * var select = (File.fs === "Macintosh") ? customShapesFileFilter : "Custom Shapes Files:*.csh,All Files:*";
204          * var customShapesFile = File.openDialog ("Select a custom shapes file:", select);
205          * if (customShapesFile !== null)
206          * {
207          *     var fileData = jamShapes.<strong>dataFromCustomShapesFile</strong> (customShapesFile, -1);
208          *     if (typeof fileData === 'string')
209          *     {
210          *         alert (fileData + "\n" + "Custom shapes file: “" + File.decode (customShapesFile.name) + "”");
211          *     }
212          *     else
213          *     {
214          *         alert ("Number of custom shapes: " + fileData["customShapes"].length);
215          *     }
216          * }
217          */
218         jamShapes.dataFromCustomShapesFile = function (shapesFile, shapeIndex)
219         {
220             function skipBytes (file, byteCount)
221             {
222                 file.seek (byteCount, 1);
223             }
224             //
225             function readBEInt (file, byteCount)
226             {
227                 var bytes = file.read (byteCount);
228                 var intValue = 0;
229                 for (var index = 0; index < byteCount; index++)
230                 {
231                     intValue = (intValue << 8) + bytes.charCodeAt (index);
232                 }
233                 return intValue;
234             }
235             //
236             function readBytes (file, byteCount)
237             {
238                 return file.read (byteCount);
239             }
240             //
241             function readPascalString (file)
242             {
243                 var stringLength = readBEInt (file, 1);
244                 return readBytes (file, stringLength);
245             }
246             //
247             function readUnicodeStringWithPadding (file)
248             {
249                 var unicodeString = "";
250                 var unicodeLength = readBEInt (file, 4);    // Includes terminating null
251                 for (var index = 0; index < unicodeLength; index++)
252                 {
253                     var unicodeChar = readBEInt (file, 2);
254                     if (unicodeChar !== 0)
255                     {
256                         unicodeString += String.fromCharCode (unicodeChar);
257                     }
258                 }
259                 if ((unicodeLength % 2) !== 0)
260                 {
261                     skipBytes (file, 2);
262                 }
263                 return unicodeString;
264             }
265             //
266             function readSignedBEInt32 (file)
267             {
268                 var intValue = readBEInt (file, 4);
269                 return (intValue < 0x80000000) ? intValue : (intValue - 0x100000000);
270             }
271             //
272             function readSignedBEFixed32 (file)
273             {
274                 return readSignedBEInt32 (file) / 0x1000000;
275             }
276             //
277             var file;
278             if (typeof shapesFile === 'string')
279             {
280                 file = new File (shapesFile);
281             }
282             else if (shapesFile instanceof File)
283             {
284                 file = shapesFile;
285             }
286             else
287             {
288                 throw new Error ('[jamShapes.dataFromCustomShapesFile] Invalid argument');
289             }
290             //
291             var selectorStrings =
292             [
293                 "closedLength",
294                 "closedLinked",
295                 "closedUnlinked",
296                 "openLength",
297                 "openLinked",
298                 "openUnlinked",
299                 "pathFill",
300                 "clipboard",
301                 "initialFill"
302             ];
303             //
304             var fileData;
305             if (file.open ("r"))
306             {
307                 try
308                 {
309                     file.encoding = 'BINARY';
310                     var magicNumber = file.read (4);
311                     if (magicNumber === 'cush')
312                     {
313                         var fileVersion = readBEInt (file, 4);
314                         if (fileVersion === 2)
315                         {
316                             fileData = { };
317                             fileData["fileVersion"] = fileVersion;
318                             var customShapes = [ ];
319                             var customShapeCount = readBEInt (file, 4);
320                             for (var customShapeIndex = 0; customShapeIndex < customShapeCount; customShapeIndex++)
321                             {
322                                 var customShape = { };
323                                 customShape["name"] = localize (readUnicodeStringWithPadding (file));
324                                 var unknown = jamUtils.dataToHexaString (readBytes (file, 4));
325                                 var dataLength = readBEInt (file, 4);
326                                 var dataStart = file.tell ();
327                                 customShape["ID"] = readPascalString (file);
328                                 if ((typeof shapeIndex === 'undefined') || (shapeIndex === customShapeIndex))
329                                 {
330                                     var top = readSignedBEInt32 (file);
331                                     var left = readSignedBEInt32 (file);
332                                     var bottom = readSignedBEInt32 (file);
333                                     var right = readSignedBEInt32 (file);
334                                     customShape["bounds"] = [ top, left, bottom, right ];
335                                     var pathRecords = [ ];
336                                     var pathRecordCount = Math.floor ((dataStart + dataLength - file.tell ()) / (2 + 8 + 8 + 8));
337                                     for (var pathRecordIndex = 0; pathRecordIndex < pathRecordCount; pathRecordIndex++)
338                                     {
339                                         var pathRecord = [ ];
340                                         var selector = readBEInt (file, 2);
341                                         if ((selector >= 0) && (selector < selectorStrings.length))
342                                         {
343                                             pathRecord.push (selectorStrings[selector]);
344                                         }
345                                         else
346                                         {
347                                             throw new Error ("[jamShapes.dataFromCustomShapesFile] Unknown selector: " + selector);
348                                         }
349                                         switch (selector)
350                                         {
351                                             case 6:
352                                                 pathRecord.push (null);
353                                                 skipBytes (file, 24);
354                                                 break;
355                                             case 8:
356                                                 pathRecord.push (readBEInt (file, 2));
357                                                 skipBytes (file, 24 - 2);
358                                                 break;
359                                             case 0:
360                                             case 3:
361                                                 pathRecord.push (readBEInt (file, 2));
362                                                 skipBytes (file, 24 - 2);
363                                                 break;
364                                             case 1:
365                                             case 2:
366                                             case 4:
367                                             case 5:
368                                                 pathRecord.push
369                                                 (
370                                                     [
371                                                         [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ],
372                                                         [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ],
373                                                         [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ]
374                                                     ]
375                                                 );
376                                                 break;
377                                             default:
378                                                 pathRecord.push (null);
379                                                 skipBytes (file, 24);
380                                                 break;
381                                         }
382                                         pathRecords.push (pathRecord);
383                                     }
384                                     customShape["pathRecords"] = pathRecords;
385                                 }
386                                 file.seek (dataStart + dataLength, 0);
387                                 customShapes.push (customShape);
388                             }
389                             fileData["customShapes"] = customShapes;
390                         }
391                         else
392                         {
393                             fileData = "Unrecognized custom shapes file version: " + fileVersion;
394                         }
395                     }
396                     else
397                     {
398                         fileData = "Unrecognized custom shapes file magic number: '" + magicNumber + "'";
399                     }
400                 }
401                 catch (e)
402                 {
403                     fileData = e.message;
404                 }
405                 finally
406                 {
407                     file.close ();
408                 }
409             }
410             else
411             {
412                 fileData = "Cannot open file";
413             }
414             return fileData;
415         };
416         //
417         /**
418          * @description Global option: if true, jamShapes.pathComponentsFromCustomShape () returns the path components of 
419          * the shape's bounding box instead of the shape itself.
420          * @type Boolean
421          * @default false
422          * @see jamShapes.pathComponentsFromCustomShape
423          * @example
424          * var fileData = jamShapes.dataFromCustomShapesFile ("~/JSON Action Manager/tests/resources/Logo-X-Aqua.csh");
425          * if (typeof fileData === 'string')
426          * {
427          *     alert (fileData);
428          * }
429          * else
430          * {
431          *     var customShape = fileData["customShapes"][0];
432          *     var bounds = [ [ 10, 10, 90, 90 ], "percentUnit" ];
433          *     jamShapes.<strong>debugMode</strong> = true;
434          *     pathComponents = jamShapes.pathComponentsFromCustomShape (customShape, "add", bounds, true);
435          *     jamEngine.jsonPlay
436          *     (
437          *         "set",
438          *         {
439          *             "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] },
440          *             "to": jamHelpers.toPathComponentList (pathComponents)
441          *         }
442          *     );
443          *     jamEngine.jsonPlay
444          *     (
445          *         "fill",
446          *         {
447          *             "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] },
448          *             "wholePath": { "<boolean>": true },
449          *             "using": { "<enumerated>": { "fillContents": "color" } },
450          *             "color": jamHelpers.nameToColorObject ("W3C", "Red"),
451          *             "opacity": { "<unitDouble>": { "percentUnit": 50 } },
452          *             "mode": { "<enumerated>": { "blendMode": "normal" } },
453          *             "radius": { "<unitDouble>": { "pixelsUnit": 0.0 } },
454          *             "antiAlias": { "<boolean>": true }
455          *         }
456          *     );
457          * }
458          */
459         jamShapes.debugMode = false;
460         //
461         /**
462          * @description Get a JSON array of data (simplified path component values) and unit ID string for coordinates 
463          * from a custom shape data structure obtained from a converted custom shapes file (*.csh).
464          * @param {Object} customShape Custom shape data structure in JSON format, obtained from a converted custom shapes file (*.csh)
465          * @param {String} shapeOperation Shape operation:<br />
466          * <ul>
467          * <li>"add"</li>
468          * <li>"subtract"</li>
469          * <li>"intersect"</li>
470          * <li>"xor"</li>
471          * </ul>
472          * @param {Array} bounds Path bounds rectangle with optional unit (either "distanceUnit" or "percentUnit" or "pixelsUnit"):<br />
473          * [ [ left, top, right, bottom ], unit ]
474          * @param {Boolean} [constrainProportions] Constrain proportions using the custom shape aspect ratio (false by default)
475          * @returns {Array} JSON array of data (simplified path component values) and unit ID string for coordinates
476          * (cf. <a href="http://www.tonton-pixel.com/blog/json-photoshop-scripting/json-simplified-formats/path-component-list-simplified-format/">Path Component List Simplified Format</a>);
477          * if jamShapes.debugMode is true, returns the path components of the shape's bounding box instead of the shape itself
478          * @see jamShapes.dataFromCustomShapesFile
479          * @see jamShapes.debugMode
480          * @example
481          * var fileData = jamShapes.dataFromCustomShapesFile ("~/JSON Action Manager/tests/resources/Logo-X-Aqua.csh");
482          * if (typeof fileData === 'string')
483          * {
484          *     alert (fileData);
485          * }
486          * else
487          * {
488          *     var customShape = fileData["customShapes"][0];
489          *     var bounds = [ [ 10, 10, 90, 90 ], "percentUnit" ];
490          *     var pathComponents = jamShapes.<strong>pathComponentsFromCustomShape</strong> (customShape, "add", bounds, true);
491          *     jamEngine.jsonPlay
492          *     (
493          *         "set",
494          *         {
495          *             "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] },
496          *             "to": jamHelpers.toPathComponentList (pathComponents)
497          *         }
498          *     );
499          * }
500          */
501         jamShapes.pathComponentsFromCustomShape = function (customShape, shapeOperation, bounds, constrainProportions)
502         {
503             var rectangle = bounds[0];
504             var unit = bounds[1];   // Optional, may be undefined
505             var left = rectangle[0];
506             var top = rectangle[1];
507             var right = rectangle[2];
508             var bottom = rectangle[3];
509             var width = right - left;
510             var height = bottom - top;
511             if (constrainProportions)
512             {
513                 var adjustmentFactor = 1;
514                 if ((typeof unit !== 'undefined') && (jamEngine.uniIdStrToId (unit) === jamEngine.uniIdStrToId ("percentUnit")))
515                 {
516                     var saveMeaningfulIds = jamEngine.meaningfulIds;
517                     var saveParseFriendly = jamEngine.parseFriendly;
518                     jamEngine.meaningfulIds = true;
519                     jamEngine.parseFriendly = true;
520                     var resultDescObj = jamEngine.jsonGet ([ { "document": [ "<enumerated>", [ "ordinal", "first" ] ] } ]);
521                     jamEngine.meaningfulIds = saveMeaningfulIds;
522                     jamEngine.parseFriendly = saveParseFriendly;
523                     adjustmentFactor = resultDescObj["width"][1][1] / resultDescObj["height"][1][1];
524                 }
525                 var boundsRatio = (width / height) * adjustmentFactor;
526                 var shapeWidth = customShape["bounds"][3] - customShape["bounds"][1];
527                 var shapeHeight = customShape["bounds"][2] - customShape["bounds"][0];
528                 var shapeRatio = shapeWidth / shapeHeight;
529                 if (shapeRatio > boundsRatio)
530                 {
531                     shapeHeight = (width / shapeRatio) * adjustmentFactor;
532                     top += (height - shapeHeight) / 2;
533                     height = shapeHeight;
534                 }
535                 else
536                 {
537                     shapeWidth = (height * shapeRatio) / adjustmentFactor;
538                     left += (width - shapeWidth) / 2;
539                     width = shapeWidth;
540                 }
541             }
542             var subpaths = [ ];
543             if (this.debugMode)
544             {
545                 var subpath =
546                 [
547                     [ [ left, top ] ],
548                     [ [ left + width, top ] ],
549                     [ [ left + width, top + height ] ],
550                     [ [ left, top + height ] ]
551                 ];
552                 subpaths.push ([ subpath, true ]);
553             }
554             else
555             {
556                 var pathRecords = customShape["pathRecords"];
557                 var subLength = 0;
558                 for (var pathRecordIndex = 0; pathRecordIndex < pathRecords.length; pathRecordIndex++)
559                 {
560                     var pathRecord = pathRecords[pathRecordIndex];
561                     var selector = pathRecord[0];
562                     var data = pathRecord[1];
563                     switch (selector)
564                     {
565                         case "closedLength":
566                         case "openLength":
567                             subLength = data;
568                             var closedSubpath = (selector === "closedLength");
569                             var subpath = [ ];
570                             break;
571                         case "closedLinked":
572                         case "closedUnlinked":
573                         case "openLinked":
574                         case "openUnlinked":
575                             var backward =
576                             [
577                                 left + (data[0][1] * width),
578                                 top + (data[0][0] * height)
579                             ];
580                             var anchor =
581                             [
582                                 left + (data[1][1] * width),
583                                 top + (data[1][0] * height)
584                             ];
585                             var forward =
586                             [
587                                 left + (data[2][1] * width),
588                                 top + (data[2][0] * height)
589                             ];
590                             var smooth = (selector === "closedLinked") || (selector === "openLinked");
591                             subpath.push ([ anchor, forward, backward, smooth ]);
592                             if (--subLength === 0)
593                             {
594                                 subpaths.push ([ subpath, closedSubpath ]);
595                             }
596                             break;
597                     }
598                 }
599             }
600             var pathComponentsArr = [ [ [ shapeOperation, subpaths ] ] ];
601             if (unit)
602             {
603                 pathComponentsArr.push (unit);
604             }
605             return pathComponentsArr;
606         };
607     } ());
608 }
609 
610 //------------------------------------------------------------------------------
611 
612