├── .gitignore ├── AEFT └── anchorReceiver.js ├── Outliner.jsx ├── README.md ├── assets ├── example.png ├── example2A.png └── example2B.png └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | AEFT/ 2 | sandbox/ -------------------------------------------------------------------------------- /AEFT/anchorReceiver.js: -------------------------------------------------------------------------------- 1 | // IN TRANSFORM > POSITION 2 | const parentLayer = thisComp.layer("line"); 3 | const parentPath = parentLayer.content("line").content("Path 1").path; 4 | let vertices = [0]; 5 | 6 | const data = { 7 | vertex: parentPath.points()[1] 8 | }; 9 | 10 | vertices.map((index, i) => { 11 | let offsetX = 0; 12 | let offsetY = 0; 13 | Object.keys(data).forEach((val, e) => { 14 | offsetX = offsetX + data[val][0]; 15 | offsetY = offsetY + data[val][1]; 16 | }); 17 | return [offsetX, offsetY]; 18 | })[0]; 19 | 20 | const parentLayer = thisComp.layer("line");const parentPath = parentLayer.content("line 1").Path 1.path;let vertices = [0];const data = { vertex: parentPath.points()[0]};vertices.map((index, i) => { let offsetX = 0; let offsetY = 0; Object.keys(data).forEach((val, e) => { offsetX = offsetX + data[val][0]; offsetY = offsetY + data[val][1]; }); return [offsetX, offsetY];})[0]; -------------------------------------------------------------------------------- /Outliner.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: 3 | -- Additional font support? 4 | 5 | https://github.com/Inventsable/Outliner 6 | contact: tom@inventsable.cc 7 | 8 | Barebones script to convert all paths in current document to permanent Outlines, including handles and anchors. 9 | This action can be undone with a single Edit > Undo command. 10 | 11 | You can edit the below settings: 12 | */ 13 | var anchorWidth = 4; // number in pixels, width of stroke 14 | var anchorSize = 20; // number in pixels, height/width of rectangle 15 | var handleSize = 25; // number in pixels, size of ellipse/orb where handle is grabbed 16 | var anchorColor = newRGB(50, 50, 200); // RGB value, defaults to blue 17 | var anchorIsFilled = false; // Boolean, if true anchors are filled, otherwise have only stroke 18 | // 19 | var parentGroupLabel = "_nodes"; 20 | var anchorLabel = "_anchor"; 21 | var handleLabel = "_handle"; 22 | var stickLabel = "_stick"; 23 | // 24 | var outlineWidth = 5; // number in pixels, width of stroke 25 | var outlineColor = newRGB(35, 31, 32); // The RGB value of color (default rich black) 26 | // 27 | var useLayerLabelColor = true; // Boolean, if true override above anchorColor and use the Layer's label instead 28 | var forceOpacity = true; // Boolean, if true force all paths to have full opacity 29 | var overrideComplex = false; // Boolean, if true clone all objects and attempt to reconstruct them 30 | // This only needs to be true if you have complex Appearances like multiple strokes per object 31 | var mergeClippingMasks = true; // Boolean, if true will use Pathfinder > Intersect on all Clipping Masks and contents 32 | // If merging is true, requires an additional Undo command per item merged to get back to original 33 | var renameGenericPaths = true; // Boolean, if true will rename unnamed paths as their parent layer 34 | var generateIds = false; // Boolean, if true with generate names with 3 character unique identifiers 35 | var groupRelated = true; // Boolean, if true create child groups for each handle within a parent group for anchor and both handles 36 | /* 37 | 38 | Do not edit below unless you know what you're doing! 39 | 40 | */ 41 | 42 | convertAllToOutlines(); 43 | var doc = app.activeDocument; 44 | 45 | function convertAllToOutlines() { 46 | convertListToOutlines(scanCurrentPageItems()); 47 | sortLayerContents(); 48 | } 49 | 50 | // Return a list of current pathItems in activeDoc or their clones when overriding complex appearance 51 | function scanCurrentPageItems() { 52 | var list = []; 53 | if (!overrideComplex) { 54 | if (mergeClippingMasks) mergeClippingPaths(); 55 | for (var i = app.activeDocument.pathItems.length - 1; i >= 0; i--) 56 | list.push(app.activeDocument.pathItems[i]); 57 | return list; 58 | } else { 59 | return cloneAllPathItems(); 60 | } 61 | } 62 | 63 | function convertListToOutlines(list) { 64 | for (var i = list.length - 1; i >= 0; i--) { 65 | var item = list[i]; 66 | item.name = renameGenericPaths 67 | ? rollName( 68 | item.name || item.parent.name || item.layer.name, 69 | item, 70 | item.layer 71 | ) 72 | : item.name || item.parent.name || item.layer.name; 73 | if (item.stroked || item.filled) { 74 | replaceAppearance(item); 75 | var parentgroup = groupRelated 76 | ? app.activeDocument.groupItems.add() 77 | : null; 78 | if (groupRelated) { 79 | parentgroup.name = item.name + parentGroupLabel; 80 | parentgroup.move(item.layer, ElementPlacement.PLACEATBEGINNING); 81 | } 82 | if (item.pathPoints && item.pathPoints.length) 83 | for (var p = 0; p < item.pathPoints.length; p++) { 84 | var point = item.pathPoints[p]; 85 | var pointName = item.name + "[" + p + "]"; 86 | var group = groupRelated ? parentgroup.groupItems.add() : null; 87 | if (groupRelated) group.name = pointName; 88 | drawAnchor(point, item.layer, pointName, group); 89 | drawHandle(point, "left", item.layer, pointName, group); 90 | drawHandle(point, "right", item.layer, pointName, group); 91 | item.opacity = forceOpacity ? 100.0 : item.opacity; 92 | } 93 | } 94 | } 95 | } 96 | 97 | function drawAnchor(point, layer, name, group) { 98 | var anchor = groupRelated 99 | ? group.pathItems.rectangle( 100 | point.anchor[1] + anchorSize / 2, 101 | point.anchor[0] - anchorSize / 2, 102 | anchorSize, 103 | anchorSize 104 | ) 105 | : app.activeDocument.pathItems.rectangle( 106 | point.anchor[1] + anchorSize / 2, 107 | point.anchor[0] - anchorSize / 2, 108 | anchorSize, 109 | anchorSize 110 | ); 111 | anchor.name = name + anchorLabel; 112 | if (!groupRelated) anchor.move(layer, ElementPlacement.PLACEATBEGINNING); 113 | setAnchorAppearance(anchor, false, layer); 114 | return [anchor]; 115 | } 116 | function drawHandle(point, direction, layer, name, group) { 117 | if ( 118 | Number(point.anchor[0]) !== Number(point[direction + "Direction"][0]) || 119 | Number(point.anchor[1]) !== Number(point[direction + "Direction"][1]) 120 | ) { 121 | var stick = groupRelated 122 | ? group.pathItems.add() 123 | : app.activeDocument.pathItems.add(); 124 | stick.setEntirePath([point.anchor, point[direction + "Direction"]]); 125 | if (!groupRelated) stick.move(layer, ElementPlacement.PLACEATBEGINNING); 126 | stick.name = name + "_" + direction.charAt(0).toUpperCase() + stickLabel; 127 | setAnchorAppearance(stick, true, layer); 128 | var handle = groupRelated 129 | ? group.pathItems.ellipse( 130 | point[direction + "Direction"][1] + handleSize / 2, 131 | point[direction + "Direction"][0] - handleSize / 2, 132 | handleSize, 133 | handleSize 134 | ) 135 | : app.activeDocument.pathItems.ellipse( 136 | point[direction + "Direction"][1] + handleSize / 2, 137 | point[direction + "Direction"][0] - handleSize / 2, 138 | handleSize, 139 | handleSize 140 | ); 141 | if (!groupRelated) handle.move(layer, ElementPlacement.PLACEATBEGINNING); 142 | handle.stroked = false; 143 | handle.filled = true; 144 | handle.name = name + "_" + direction.charAt(0).toUpperCase() + handleLabel; 145 | handle.fillColor = useLayerLabelColor ? layer.color : anchorColor; 146 | return [stick, handle]; 147 | } 148 | } 149 | 150 | function setAnchorAppearance(item, isHandle, layer) { 151 | var realColor = useLayerLabelColor ? layer.color : anchorColor; 152 | if (!isHandle) { 153 | item.filled = anchorIsFilled; 154 | item.stroked = !anchorIsFilled; 155 | if (!anchorIsFilled) { 156 | item.strokeWidth = anchorWidth; 157 | item.strokeColor = realColor; 158 | } else { 159 | item.fillColor = realColor; 160 | } 161 | } else { 162 | item.filled = false; 163 | item.stroked = true; 164 | item.strokeWidth = anchorWidth; 165 | item.strokeColor = realColor; 166 | } 167 | } 168 | 169 | function replaceAppearance(item) { 170 | item.filled = false; 171 | item.stroked = true; 172 | item.strokeWidth = outlineWidth; 173 | item.strokeColor = outlineColor; 174 | } 175 | 176 | function newRGB(r, g, b) { 177 | var color = new RGBColor(); 178 | color.red = r; 179 | color.green = g; 180 | color.blue = b; 181 | return color; 182 | } 183 | 184 | // Rearrange results per layer so anchor Groups are directly above their target path 185 | function sortLayerContents() { 186 | for (var i = 0; i < app.activeDocument.layers.length; i++) { 187 | var layer = app.activeDocument.layers[i]; 188 | for (var c = 0; c < layer.pathItems.length; c++) 189 | layer.pathItems[c].zOrder(ZOrderMethod.BRINGTOFRONT); 190 | var offset = layer.pathItems.length + 1; 191 | for (var c = 0; c < layer.groupItems.length; c++) { 192 | var group = layer.groupItems[c]; 193 | offset = Number(offset) - Number(1); 194 | for (var z = 0; z < offset; z++) group.zOrder(ZOrderMethod.BRINGFORWARD); 195 | } 196 | } 197 | } 198 | 199 | // Generates a unique identifier for layer to use in children nodes 200 | function rollName(name, item, layer) { 201 | var siblingCount = 0; 202 | var nameRX = new RegExp(name + "\\[\\d\\].*"); 203 | if (!generateIds) 204 | for (var i = 0; i < layer.pathItems.length; i++) 205 | if ( 206 | nameRX.test(layer.pathItems[i].name) && 207 | layer.pathItems[i] !== item && 208 | !/group/i.test(layer.pathItems[i].typename) 209 | ) 210 | siblingCount++; 211 | return generateIds 212 | ? name + "_" + shortId() + "_" 213 | : name + "[" + siblingCount + "]"; 214 | } 215 | 216 | // Reconstruct all PathItems with basic data to override any complex appearances 217 | function cloneAllPathItems() { 218 | var list = []; 219 | var cloneProps = ["position", "left", "top", "name", "closed"]; 220 | var pathProps = ["anchor", "leftDirection", "rightDirection", "pointType"]; 221 | for (var i = app.activeDocument.pathItems.length - 1; i >= 0; i--) { 222 | var item = app.activeDocument.pathItems[i]; 223 | var clone = { 224 | pathPoints: [], 225 | }; 226 | for (var v = 0; v < cloneProps.length; v++) { 227 | var prop = cloneProps[v]; 228 | clone[prop] = item[prop]; 229 | } 230 | for (var v = 0; v < item.pathPoints.length; v++) 231 | clone.pathPoints.push(item.pathPoints[v]); 232 | list.push(clone); 233 | item.remove(); 234 | } 235 | var dupes = []; 236 | for (var i = 0; i < list.length; i++) { 237 | var schema = list[i]; 238 | var item = app.activeDocument.pathItems.add(); 239 | for (var v = 0; v < cloneProps.length; v++) { 240 | var prop = cloneProps[v]; 241 | item[prop] = schema[prop]; 242 | } 243 | for (var v = 0; v < schema.pathPoints.length; v++) { 244 | var point = schema.pathPoints[v]; 245 | var newpoint = item.pathPoints.add(); 246 | for (var c = 0; c < pathProps.length; c++) { 247 | var prop = pathProps[c]; 248 | newpoint[prop] = point[prop]; 249 | } 250 | } 251 | dupes.push(item); 252 | } 253 | return dupes; 254 | } 255 | 256 | function mergeClippingPaths() { 257 | app.selection = null; 258 | app.executeMenuCommand("Clipping Masks menu item"); 259 | var masks = app.selection; 260 | if (app.selection.length < 1) return null; 261 | for (var i = 0; i < masks.length; i++) { 262 | var mask = masks[i]; 263 | var parent = mask.parent; 264 | var siblings = []; 265 | for (var v = 0; v < parent.pathItems.length; v++) { 266 | var child = parent.pathItems[v]; 267 | if (!child.clipping) { 268 | // var tag = child.tags.add(); 269 | // tag.name = "marked"; 270 | siblings.push(child); 271 | } 272 | } 273 | if (siblings.length > 1) 274 | for (var v = 1; v < siblings.length; v++) { 275 | app.selection = null; 276 | var dupe = mask.duplicate(); 277 | var sibling = siblings[v]; 278 | var lastname = sibling.name; 279 | dupe.selected = true; 280 | sibling.selected = true; 281 | intersectAction(); 282 | // 283 | // TODO 284 | // If path has name, doing intersect creates a new path and this reference is lost. 285 | // 286 | } 287 | app.selection = null; 288 | mask.selected = true; 289 | siblings[0].selected = true; 290 | var lastname = siblings[0].name; 291 | intersectAction(); 292 | app.selection = null; 293 | // 294 | // Fix name transfer 295 | // 296 | parent.selected = true; 297 | app.executeMenuCommand("ungroup"); 298 | app.selection = null; 299 | } 300 | } 301 | 302 | // Thanks Qwertyfly 303 | // https://community.adobe.com/t5/illustrator/js-cs6-executemenucommand/m-p/5904772#M19673 304 | function intersectAction() { 305 | if ((app.documents.length = 0)) { 306 | return; 307 | } 308 | var ActionString = [ 309 | "/version 3", 310 | "/name [ 10", 311 | " 4578706f727454657374", 312 | "]", 313 | "/isOpen 1", 314 | "/actionCount 1", 315 | "/action-1 {", 316 | " /name [ 9", 317 | " 496e74657273656374", 318 | " ]", 319 | " /keyIndex 0", 320 | " /colorIndex 0", 321 | " /isOpen 1", 322 | " /eventCount 1", 323 | " /event-1 {", 324 | " /useRulersIn1stQuadrant 0", 325 | " /internalName (ai_plugin_pathfinder)", 326 | " /localizedName [ 10", 327 | " 5061746866696e646572", 328 | " ]", 329 | " /isOpen 0", 330 | " /isOn 1", 331 | " /hasDialog 0", 332 | " /parameterCount 1", 333 | " /parameter-1 {", 334 | " /key 1851878757", 335 | " /showInPalette -1", 336 | " /type (enumerated)", 337 | " /name [ 9", 338 | " 496e74657273656374", 339 | " ]", 340 | " /value 1", 341 | " }", 342 | " }", 343 | "}", 344 | ].join("\n"); 345 | createAction(ActionString); 346 | var ActionString = null; 347 | app.doScript("Intersect", "ExportTest", false); 348 | app.unloadAction("ExportTest", ""); 349 | function createAction(str) { 350 | var f = new File("~/ScriptAction.aia"); 351 | f.open("w"); 352 | f.write(str); 353 | f.close(); 354 | app.loadAction(f); 355 | f.remove(); 356 | } 357 | } 358 | 359 | function randomInt(min, max) { 360 | return Math.floor(Math.random() * (max - min + 1) + min); 361 | } 362 | 363 | function shortId() { 364 | var str = ""; 365 | var codex = "0123456789abcdefghijklmnopqrstuvwxyz"; 366 | for (var i = 0; i <= 2; i++) 367 | str += codex.charAt(randomInt(0, codex.length - 1)); 368 | return str.toUpperCase(); 369 | } 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outliner 2 | 3 | Illustrator script to convert all artwork to a "permanent outline" mode, replacing all Appearances and manually drawing all anchors/handles: 4 | 5 | _Result of most basic options on left, original artwork on right_ 6 | 7 | ![](./assets/example.png) 8 | 9 | - Variable sizes for all results including anchors, strokes, outlines, handles. 10 | - Customizeable color for all outlines and anchors/handles or optional inherit parent layer label color 11 | - Smart name assignment with customizeable variables per item 12 | - Smart handle generation only on points needing them 13 | - Optional override complex appearances (stacked strokes/fills) 14 | - Optional merge Clipping Masks (perform intersect on all mask children) 15 | - Optional override opacity of any path 16 | - Optional deep grouping per handle and collection of handle groups per anchor 17 | - Smart sorting: anchors/handles appear in same Layer as target and just above it's zOrder 18 | 19 | ## Before 20 | 21 | _Artwork with various clipping masks and unnamed paths_ 22 | 23 | ![](./assets/example2A.png) 24 | 25 | ## After 26 | 27 | _Outliner can merge all masks, inherit layer label color, rename all targets, deeply organize them by handle and anchor_ 28 | 29 | ![](./assets/example2B.png) 30 | 31 | --- 32 | 33 | Created for practice, and by request of a reddit thread: [Is there a way to export an outline view + anchorpoints?](https://www.reddit.com/r/AdobeIllustrator/comments/e0nh4m/is_there_a_way_to_export_an_outline_view/) 34 | 35 | [Adobe thread explaining certain issues I ran into and solutions once solved](https://community.adobe.com/t5/illustrator/practice-script-to-convert-art-to-quot-permanent-outlines-quot-drawing-anchors-and-handles-what-s/td-p/10759175) 36 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inventsable/Outliner/393e08dd7bfc61c5296beccdce143658dd0cf49c/assets/example.png -------------------------------------------------------------------------------- /assets/example2A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inventsable/Outliner/393e08dd7bfc61c5296beccdce143658dd0cf49c/assets/example2A.png -------------------------------------------------------------------------------- /assets/example2B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inventsable/Outliner/393e08dd7bfc61c5296beccdce143658dd0cf49c/assets/example2B.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outliner", 3 | "version": "1.0.1", 4 | "description": "Illustrator script to convert all artwork to a \"permanent outline\" mode, replacing all Appearances and manually drawing all anchors/handles:", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Inventsable/Outliner.git" 12 | }, 13 | "keywords": [], 14 | "author": "Thomas Scharstein II ", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/Inventsable/Outliner/issues" 18 | }, 19 | "homepage": "https://github.com/Inventsable/Outliner#readme" 20 | } --------------------------------------------------------------------------------