├── README.md ├── Export for Origami.sketchplugin └── Contents │ └── Sketch │ ├── manifest.json │ └── Export for Origami.cocoascript ├── LICENSE.md └── Embedded └── Export for Origami.sketchplugin /README.md: -------------------------------------------------------------------------------- 1 | # As of October 2016, this plugin is deprecated. 2 | -------------------------------------------------------------------------------- /Export for Origami.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Export for Origami", 3 | "description": "Updates assets for importing into Origami.", 4 | "author": "Julius Tarng", 5 | "homepage": "http://github.com/tarngerine/sketch-origami-export", 6 | "version": 1.3, 7 | "identifier": "com.tarng.sketch-export-origami", 8 | "updateURL": "https://github.com/tarngerine/sketch-origami-export", 9 | "compatibleVersion": 3.3, 10 | "bundleVersion": 1, 11 | "commands": 12 | [ 13 | { 14 | "name": "Export for Origami", 15 | "identifier": "export", 16 | "shortcut": "ctrl alt cmd o", 17 | "script": "Export for Origami.cocoascript", 18 | "handler": "export_for_origami" 19 | }, 20 | { 21 | "name": "Reveal Exports in Finder", 22 | "identifier": "reveal", 23 | "script": "Export for Origami.cocoascript", 24 | "handler": "reveal_in_finder" 25 | }, 26 | ], 27 | "menu": 28 | { 29 | "items": 30 | [ 31 | "export", 32 | "reveal" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | LICENSE AGREEMENT FOR SKETCH-FRAMER PLUGIN/SKETCH-ORIGAMI-EXPORT 2 | 3 | Copyright (c) 2013-2015, Ale Muñoz, Cemre Güngor, Julius Tarng All rights reserved. 4 | 5 | BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | - Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | - Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Embedded/Export for Origami.sketchplugin: -------------------------------------------------------------------------------- 1 | // (ctrl alt cmd O) 2 | // Export for Origami v1.1 - Julius Tarng 3 | // Based on bomberstudio + cemre's sketch-framer 4 | // - Exports all groups (append name with * to flatten subgroups) and layers (appended with +), similar to sketch-framer plugin 5 | // - Saves all assets to Sketch's temp directory under Origami Projects/[doc displayName] 6 | 7 | preflight(); 8 | var debug = false; 9 | var origami_directory = "Origami Blueprints/" + [doc displayName].split(".sketch")[0] + ".origamiblueprint", 10 | export_directory = NSTemporaryDirectory() + origami_directory, //temp one avoiding sandboxing 11 | export_scale_factor = 1, 12 | layers_metadata = [], 13 | current_artboard = 0, 14 | exported_layer_count = 0, 15 | inside_top_level_group = false, //not artboard position shift 16 | top_level_group_pos = {"x" : 0, "y" : 0}, 17 | layer_names = []; // to prevent conflicting filenames 18 | main(); 19 | 20 | // 21 | // Main export functions 22 | // 23 | 24 | function main() { 25 | loggle("========================= Export for Origami log ========================="); 26 | [doc showMessage:"Exporting for Origami"]; 27 | 28 | var layers = [[doc currentPage] layers], 29 | fileManager = [NSFileManager defaultManager], 30 | settings_filepath = export_directory + "/data.json", 31 | settings = [[NSMutableDictionary alloc] init], // for data.json file 32 | settings_exists = [fileManager fileExistsAtPath:settings_filepath]; 33 | 34 | if ([selection count] > 0) { 35 | var has_top_level_group = false; 36 | for (var i=0; i<[selection count]; i++) { 37 | var s = [selection objectAtIndex:i]; 38 | if ([[s parentGroup] isMemberOfClass:[MSPage class]]) { 39 | has_top_level_group = true; 40 | } 41 | } 42 | if (has_top_level_group) { 43 | layers = selection; // only override with selection if selecting an artboard/top level group 44 | } 45 | } 46 | 47 | // Get scale factor 48 | export_scale_factor = 1; 49 | 50 | // Process layers 51 | for (var i=0; i<[layers count]; i++) { 52 | var layer = [layers objectAtIndex:i]; 53 | var group_name = ""; 54 | if (is_group(layer)) { 55 | group_name = [layer name]; 56 | inside_top_level_group = true; 57 | top_level_group_pos = calculate_real_position_for(layer); 58 | } 59 | var p = process_layer(layer, 0, group_name); 60 | if (p != undefined) { 61 | layers_metadata.push(p); 62 | } 63 | 64 | // Reset hiding on all children after all exports so dimensions are correct for groups with hidden children 65 | loggle("Finished processing layer<" + [layer name] + ">. Resetting hiding on all children"); 66 | for (var j=0; j<[[layer children] count]; j++) { 67 | var child = [[layer children] objectAtIndex:j]; 68 | if ([child name].indexOf("@@hidden") > -1]) { 69 | [child setIsVisible:false]; 70 | [child setName:[child name].split("@@hidden")[0])]; 71 | } 72 | } 73 | } 74 | 75 | [doc showMessage:"Exporting " + layer_names.length + " assets for Origami"]; 76 | 77 | // Write settings 78 | [settings setValue:export_scale_factor forKey:@"scale"]; 79 | [settings setValue:layers_metadata forKey:@"layers"]; 80 | [settings setValue:exported_layer_count forKey:@"layer_count"]; 81 | var scaleJSON = [NSJSONSerialization dataWithJSONObject:settings options:NSJSONWritingPrettyPrinted error:nil]; 82 | scaleJSON = [[NSString alloc] initWithData:scaleJSON encoding:NSUTF8StringEncoding]; 83 | loggle("Making data.json file with contents: " + scaleJSON); 84 | [scaleJSON writeToFile:settings_filepath atomically:true encoding:NSUTF8StringEncoding error:null]; 85 | 86 | // Print path for Origami use 87 | log(export_directory); 88 | } 89 | 90 | function process_layer(layer, depth, artboard_name) { 91 | var layer_data; 92 | 93 | // Process groups (including artboards) and layers marked with +, and ungrouped layers outside of artboards 94 | if (should_export_layer(layer) || (!is_group(layer) && (depth == 0) && !(should_ignore_layer(layer)))) { 95 | // Duplicate name check 96 | check_name_conflict(layer, depth, artboard_name); 97 | layer_names.push(artboard_name + [layer name]); 98 | 99 | if ([layer isVisible] == 0) { 100 | loggle("Layer hidden, showing layer addingnd adding hidden suffix"); 101 | layer_hidden = true; 102 | [layer setIsVisible:true]; 103 | [layer setName:[layer name] + "@@hidden"]; 104 | } 105 | 106 | var e = export_layer(layer, depth, artboard_name); 107 | if (e != undefined) { 108 | layer_data = e; 109 | } 110 | 111 | // Recursively go through sublayers if group not flattened with * 112 | if (is_group(layer) && !should_flatten_layer(layer) && !is_symbol(layer)) { 113 | var sublayers = [layer layers]; 114 | var has_exportable_children = false; 115 | var layers_holder = [] 116 | // Sketch returns sublayers in reverse, so we'll iterate backwards 117 | for (var sub=([sublayers count] - 1); sub >= 0; sub--) { 118 | var current = [sublayers objectAtIndex:sub]; 119 | var d = process_layer(current, depth+1, artboard_name); 120 | if (d != undefined) { 121 | layers_holder.push(d); 122 | has_exportable_children = true; 123 | } 124 | } 125 | if (has_exportable_children) { 126 | if (e != undefined) { // if group itself had an exported png, add to sublayers 127 | layers_holder.push(e) 128 | } 129 | layer_data = metadata_for(layer, layer); 130 | layer_data.type = "group"; 131 | layer_data.layers = layers_holder; 132 | } 133 | } 134 | } 135 | 136 | return layer_data; 137 | } 138 | 139 | function export_layer(layer, depth, artboard_name) { 140 | // Copy off-screen, out of artboard so it is not masked by artboard 141 | var layer_copy = [layer duplicate]; 142 | [layer_copy removeFromParent]; 143 | [[doc currentPage] addLayers: [layer_copy]]; 144 | var frame = [layer_copy frame]; 145 | [frame setX: -999999]; 146 | [frame setY: -999999]; 147 | var included_layers = []; 148 | var has_art = false; 149 | var mask_layer; 150 | var layer_data; 151 | 152 | log_depth("Processing <" + [layer name] + "> of type <" + [layer className] + ">", depth); 153 | if (is_group(layer) && !is_symbol(layer)) { 154 | var sublayers = [layer_copy layers]; 155 | for (var sub = ([sublayers count] - 1); sub >= 0; sub--) { 156 | var sublayer = [sublayers objectAtIndex:sub]; 157 | 158 | log_depth_core("Processing sublayer <" + [sublayer name] + ">", depth, " "); 159 | // Check for mask 160 | if ([sublayer hasClippingMask]) { 161 | log_depth_core("Masking with <" + [sublayer name] + ">", depth, " "); 162 | mask_layer = sublayer; 163 | } 164 | // If sublayer should be exported on its own 165 | if((should_export_layer(sublayer) && !should_flatten_layer(layer)) || [sublayer isVisible] == 0) { 166 | log_depth_core("Removing <" + [sublayer name] + ">", depth, " "); 167 | [sublayer removeFromParent]; 168 | } else { 169 | log_depth_core("Keeping <" + [sublayer name] + ">", depth, " "); 170 | included_layers.push([sublayer name]); 171 | has_art = true; 172 | } 173 | 174 | } 175 | log_depth_core("Finished processing sublayers", depth, " "); 176 | } else { 177 | has_art = true; 178 | } 179 | 180 | log_depth("Has art: " + has_art, depth); 181 | 182 | if (has_art) { 183 | // Metadata 184 | layer_data = metadata_for(layer, layer_copy); 185 | layer_data.type = "layer"; 186 | exported_layer_count++; 187 | 188 | // Export PNG 189 | if (artboard_name !== "") { 190 | artboard_name = "/" + sanitize_filename(artboard_name); 191 | } 192 | var path_to_file = export_directory + artboard_name + "/" + sanitize_filename([layer name]) + ".png"; 193 | log_depth("Exporting <" + path_to_file + "> including sublayers (" + included_layers.join(", ") + ")", depth); 194 | var rect; 195 | if (mask_layer) { 196 | rect = [MSSliceTrimming trimmedRectForSlice:mask_layer]; 197 | } else { 198 | rect = [MSSliceTrimming trimmedRectForSlice:layer_copy]; 199 | } 200 | var slice = [MSExportRequest requestWithRect:rect scale:export_scale_factor]; 201 | [doc saveArtboardOrSlice:slice toFile:path_to_file]; 202 | } else { 203 | log_depth("Did not export <" + [layer name] + ">, no image", depth); 204 | } 205 | 206 | [layer_copy removeFromParent]; 207 | return layer_data; 208 | } 209 | 210 | function metadata_for(layer, layer_copy) { 211 | loggle("Getting metadata for " + [layer name]); 212 | var cgrect = [MSSliceTrimming trimmedRectForSlice:layer_copy], 213 | position = calculate_real_position_for(layer), 214 | x,y,w,h, 215 | layer_hidden = [layer name].indexOf("@@hidden") > -1; 216 | 217 | x = position.x; 218 | y = position.y; 219 | w = cgrect.size.width; 220 | h = cgrect.size.height; 221 | 222 | // If this is an artboard we should ignore its position (at least for now) 223 | if (is_artboard(layer)) { 224 | loggle("Resetting x and y to 0 because artboard"); 225 | x = 0; 226 | y = 0; 227 | } 228 | 229 | loggle("Metadata for <" + [layer name] + ">: { x:"+x+", y:"+y+", width:"+w+", height:"+h+"}"); 230 | return { 231 | x: x, 232 | y: y, 233 | w: w, 234 | h: h, 235 | name : sanitize_filename([layer name]), 236 | hidden : layer_hidden 237 | }; 238 | } 239 | 240 | function calculate_real_position_for(layer) { 241 | var cgrect = [MSSliceTrimming trimmedRectForSlice:layer], 242 | absrect = [layer absoluteRect]; 243 | var rulerDeltaX = [absrect rulerX] - [absrect x], 244 | rulerDeltaY = [absrect rulerY] - [absrect y], 245 | CGRectRulerX = cgrect.origin.x + rulerDeltaX, 246 | CGRectRulerY = cgrect.origin.y + rulerDeltaY; 247 | return { 248 | x: Math.round(CGRectRulerX), 249 | y: Math.round(CGRectRulerY) 250 | } 251 | } 252 | 253 | // 254 | // Helpers 255 | // 256 | function preflight() { 257 | var app_version = [NSApp applicationVersion].substr(0,1); 258 | if (app_version < 3) { 259 | alert("Export for Origami only supports Sketch 3 and above. You are running " + app_version, ". Please upgrade Sketch."); 260 | return; 261 | } 262 | } 263 | 264 | function combobox(msg, items, selectedItemIndex){ 265 | selectedItemIndex = selectedItemIndex || 0; 266 | 267 | var combobox = [[NSComboBox alloc] initWithFrame:NSMakeRect(0,0,50,25)]; 268 | [combobox addItemsWithObjectValues:items]; 269 | [combobox selectItemAtIndex:selectedItemIndex]; 270 | 271 | var alert = [[NSAlert alloc] init]; 272 | [alert setMessageText:msg]; 273 | [alert addButtonWithTitle:'Save']; 274 | [alert addButtonWithTitle:'Cancel']; 275 | [alert setAccessoryView:combobox]; 276 | 277 | var responseCode = [alert runModal]; 278 | var combosel = [combobox indexOfSelectedItem]; 279 | var combovalue = [combobox stringValue]; 280 | 281 | return [responseCode, combosel, combovalue]; 282 | } 283 | 284 | // 285 | // Layer Helpers 286 | // 287 | 288 | function should_ignore_layer(layer) { 289 | return [layer name].slice(-1) == "-"; 290 | } 291 | 292 | function should_flatten_layer(layer) { 293 | return [layer name].slice(-1) == "*"; 294 | } 295 | 296 | function should_make_layer_own_image(layer) { 297 | return [layer name].slice(-1) == '+'); 298 | } 299 | 300 | function should_export_layer(layer) { 301 | return (is_group(layer) || should_make_layer_own_image(layer)) && !should_ignore_layer(layer); 302 | } 303 | 304 | function is_group(layer) { 305 | return [layer isMemberOfClass:[MSLayerGroup class]] || [layer isMemberOfClass:[MSArtboardGroup class]] 306 | } 307 | 308 | function is_artboard(layer) { 309 | return [layer isMemberOfClass:[MSArtboardGroup class]]; 310 | } 311 | 312 | function is_symbol(layer) { 313 | if(layer.parentOrSelfIsSymbol){ 314 | return [layer parentOrSelfIsSymbol]; 315 | } else { 316 | return [layer isKindOfClass:MSSymbolInstance]; 317 | } 318 | } 319 | 320 | // 321 | // File helpers 322 | // 323 | 324 | function sanitize_filename(name) { 325 | return name.replace(/(:|\/)/g ,"_").replace(/__/g,"_").replace("*","").replace("+","").replace("@@hidden",""); // TODO: replace spaces? /(\s|:|\/)/g 326 | } 327 | 328 | function check_name_conflict(layer, depth, artboard_name) { 329 | var was_conflict = false; 330 | if (array_contains(layer_names, artboard_name + [layer name])) { 331 | was_conflict = true; 332 | log_depth("Layer name conflict: <" + [layer name] + "> already exists in artboard <" + artboard_name + ">", depth); 333 | 334 | // Preserve layer name modifiers + - * 335 | var last_char = [layer name].slice(-1); 336 | var has_modifier = should_ignore_layer(layer) || should_flatten_layer(layer) || should_make_layer_own_image(layer); 337 | if (has_modifier) { 338 | [layer setName:[layer name].split(last_char)[0]]; // Remove last_char 339 | } 340 | [layer setName:[layer name] + " copy"]; 341 | if (has_modifier) { 342 | [layer setName:[layer name] + last_char]; 343 | } 344 | log_depth("Renaming to: <" + [layer name] + ">", depth); 345 | 346 | check_name_conflict(layer, depth, artboard_name); 347 | } 348 | 349 | return was_conflict; 350 | } 351 | 352 | // 353 | // Debug Helpers 354 | // 355 | 356 | function alert(msg, title){ 357 | var app = [NSApplication sharedApplication]; 358 | [app displayDialog:msg withTitle:title]; 359 | } 360 | 361 | function log_depth(message, depth) { 362 | log_depth_core(message, depth, ">"); 363 | } 364 | 365 | function log_depth_core(message, depth, spacer) { 366 | var padding = spacer; 367 | for(var i=0; i 0) { 36 | var has_top_level_group = false; 37 | for (var i=0; i<[selection count]; i++) { 38 | var s = [selection objectAtIndex:i]; 39 | if ([[s parentGroup] isMemberOfClass:[MSPage class]]) { 40 | has_top_level_group = true; 41 | } 42 | } 43 | if (has_top_level_group) { 44 | layers = selection; // only override with selection if selecting an artboard/top level group 45 | } 46 | } 47 | 48 | // Get scale factor 49 | if (settings_exists) { 50 | var data = [[NSString stringWithContentsOfFile:settings_filepath] dataUsingEncoding:NSUTF8StringEncoding]; 51 | var settings_old = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; 52 | export_scale_factor = [settings_old valueForKeyPath:@"scale"]; 53 | } else { 54 | var factors = [".5x", "1x", "1.5x", "2x", "3x"]; 55 | var scale_factor_choice = combobox("Export with what resolution multiplier?", factors, 1); 56 | if (scale_factor_choice[0] != NSAlertFirstButtonReturn) { return; } 57 | export_scale_factor = parseFloat(scale_factor_choice[2].replace(/[^0-9.]/g,"")); 58 | } 59 | 60 | // Process layers 61 | for (var i=0; i<[layers count]; i++) { 62 | var layer = [layers objectAtIndex:i]; 63 | var group_name = ""; 64 | if (is_group(layer)) { 65 | group_name = [layer name]; 66 | } 67 | if (![layer isMemberOfClass:[MSArtboardGroup class]]) { 68 | inside_top_level_group = true; 69 | top_level_group_pos = calculate_real_position_for(layer); 70 | } 71 | var p = process_layer(layer, 0, group_name); 72 | if (p != undefined) { 73 | layers_metadata.push(p); 74 | } 75 | 76 | // Reset hiding on all children after all exports so dimensions are correct for groups with hidden children 77 | log("Finished processing layer<" + [layer name] + ">. Resetting hiding on all children"); 78 | for (var j=0; j<[[layer children] count]; j++) { 79 | var child = [[layer children] objectAtIndex:j]; 80 | if ([child name].indexOf("@@hidden") > -1]) { 81 | [child setIsVisible:false]; 82 | [child setName:[child name].split("@@hidden")[0])]; 83 | } 84 | } 85 | } 86 | 87 | // Save scale factor into settings 88 | if (settings_exists) { 89 | [doc showMessage:"Updating " + layer_names.length + " assets for Origami. Plugins > Export for Origami > Reveal Exports in Finder to see them."]; 90 | } else { 91 | [doc showMessage:"Exporting " + layer_names.length + " assets for Origami. Plugins > Export for Origami > Reveal Exports in Finder to see them."]; 92 | reveal_in_finder(context); 93 | } 94 | 95 | // Write settings 96 | [settings setValue:export_scale_factor forKey:@"scale"]; 97 | [settings setValue:layers_metadata forKey:@"layers"]; 98 | [settings setValue:exported_layer_count forKey:@"layer_count"]; 99 | var scaleJSON = [NSJSONSerialization dataWithJSONObject:settings options:NSJSONWritingPrettyPrinted error:nil]; 100 | scaleJSON = [[NSString alloc] initWithData:scaleJSON encoding:NSUTF8StringEncoding]; 101 | log("Making data.json file with contents: " + scaleJSON); 102 | [scaleJSON writeToFile:settings_filepath atomically:true encoding:NSUTF8StringEncoding error:null]; 103 | log(export_directory); 104 | } 105 | 106 | // Local helpers 107 | var process_layer = function(layer, depth, artboard_name) { 108 | var layer_data; 109 | 110 | // Process groups (including artboards) and layers marked with +, and ungrouped layers outside of artboards 111 | if (should_export_layer(layer) || (!is_group(layer) && (depth == 0) && !(should_ignore_layer(layer)))) { 112 | // Duplicate name check 113 | check_name_conflict(layer, depth, artboard_name); 114 | layer_names.push(artboard_name + [layer name]); 115 | 116 | if ([layer isVisible] == 0) { 117 | log("Layer hidden, showing layer addingnd adding hidden suffix"); 118 | layer_hidden = true; 119 | [layer setIsVisible:true]; 120 | [layer setName:[layer name] + "@@hidden"]; 121 | } 122 | 123 | var e = export_layer(layer, depth, artboard_name); 124 | if (e != undefined) { 125 | layer_data = e; 126 | } 127 | 128 | // Recursively go through sublayers if group not flattened with * 129 | if (is_group(layer) && !should_flatten_layer(layer) && !is_symbol(layer)) { 130 | var sublayers = [layer layers]; 131 | var has_exportable_children = false; 132 | var layers_holder = [] 133 | // Sketch returns sublayers in reverse, so we'll iterate backwards 134 | for (var sub=([sublayers count] - 1); sub >= 0; sub--) { 135 | var current = [sublayers objectAtIndex:sub]; 136 | var d = process_layer(current, depth+1, artboard_name); 137 | if (d != undefined) { 138 | layers_holder.push(d); 139 | has_exportable_children = true; 140 | } 141 | } 142 | if (has_exportable_children) { 143 | if (e != undefined) { // if group itself had an exported png, add to sublayers 144 | layers_holder.push(e) 145 | } 146 | layer_data = metadata_for(layer, layer); 147 | layer_data.type = "group"; 148 | layer_data.layers = layers_holder; 149 | } 150 | } 151 | } 152 | 153 | return layer_data; 154 | } 155 | 156 | var export_layer = function(layer, depth, artboard_name) { 157 | // Copy off-screen, out of artboard so it is not masked by artboard 158 | var layer_copy = [layer duplicate]; 159 | [layer_copy removeFromParent]; 160 | [[doc currentPage] addLayers: [layer_copy]]; 161 | var frame = [layer_copy frame]; 162 | [frame setX: -999999]; 163 | [frame setY: -999999]; 164 | var included_layers = []; 165 | var has_art = false; 166 | var mask_layer; 167 | var layer_data; 168 | 169 | log_depth("Processing <" + [layer name] + "> of type <" + [layer className] + ">", depth); 170 | if (is_group(layer) && !is_symbol(layer)) { 171 | var sublayers = [layer_copy layers]; 172 | for (var sub = ([sublayers count] - 1); sub >= 0; sub--) { 173 | var sublayer = [sublayers objectAtIndex:sub]; 174 | 175 | log_depth_core("Processing sublayer <" + [sublayer name] + ">", depth, " "); 176 | // Check for mask 177 | if ([sublayer hasClippingMask]) { 178 | log_depth_core("Masking with <" + [sublayer name] + ">", depth, " "); 179 | mask_layer = sublayer; 180 | } 181 | // If sublayer should be exported on its own 182 | if((should_export_layer(sublayer) && !should_flatten_layer(layer)) || [sublayer isVisible] == 0) { 183 | log_depth_core("Removing <" + [sublayer name] + ">", depth, " "); 184 | [sublayer removeFromParent]; 185 | } else { 186 | log_depth_core("Keeping <" + [sublayer name] + ">", depth, " "); 187 | included_layers.push([sublayer name]); 188 | has_art = true; 189 | } 190 | 191 | } 192 | log_depth_core("Finished processing sublayers", depth, " "); 193 | } else { 194 | has_art = true; 195 | } 196 | 197 | log_depth("Has art: " + has_art, depth); 198 | 199 | if (has_art) { 200 | // Metadata 201 | layer_data = metadata_for(layer, layer_copy); 202 | layer_data.type = "layer"; 203 | exported_layer_count++; 204 | 205 | // Export PNG 206 | if (artboard_name !== "") { 207 | artboard_name = "/" + sanitize_filename(artboard_name); 208 | } 209 | var path_to_file = export_directory + artboard_name + "/" + sanitize_filename([layer name]) + ".png"; 210 | log_depth("Exporting <" + path_to_file + "> including sublayers (" + included_layers.join(", ") + ")", depth); 211 | var rect; 212 | if (mask_layer) { 213 | rect = [MSSliceTrimming trimmedRectForSlice:mask_layer]; 214 | } else { 215 | rect = [MSSliceTrimming trimmedRectForSlice:layer_copy]; 216 | } 217 | var slice = [MSExportRequest requestWithRect:rect scale:export_scale_factor]; 218 | [doc saveArtboardOrSlice:slice toFile:path_to_file]; 219 | } else { 220 | log_depth("Did not export <" + [layer name] + ">, no image", depth); 221 | } 222 | 223 | [layer_copy removeFromParent]; 224 | return layer_data; 225 | } 226 | 227 | 228 | var metadata_for = function(layer, layer_copy) { 229 | log("Getting metadata for " + [layer name]); 230 | var cgrect = [MSSliceTrimming trimmedRectForSlice:layer_copy], 231 | position = calculate_real_position_for(layer), 232 | x,y,w,h, 233 | layer_hidden = [layer name].indexOf("@@hidden") > -1; 234 | 235 | x = position.x; 236 | y = position.y; 237 | w = cgrect.size.width; 238 | h = cgrect.size.height; 239 | 240 | if ([layer isMemberOfClass:[MSArtboardGroup class]]) { 241 | log("Resetting x and y to 0 because artboard"); 242 | x = 0; 243 | y = 0; 244 | } 245 | if (inside_top_level_group) { 246 | x-= top_level_group_pos.x; 247 | y-= top_level_group_pos.y; 248 | log("Shifting x by: " + top_level_group_pos.x); 249 | log("Shifting y by: " + top_level_group_pos.y); 250 | } 251 | log("Metadata for <" + [layer name] + ">: { x:"+x+", y:"+y+", width:"+w+", height:"+h+"}"); 252 | return { 253 | x: x, 254 | y: y, 255 | w: w, 256 | h: h, 257 | name : sanitize_filename([layer name]), 258 | hidden : layer_hidden 259 | }; 260 | } 261 | 262 | var calculate_real_position_for = function(layer) { 263 | var cgrect = [MSSliceTrimming trimmedRectForSlice:layer], 264 | absrect = [layer absoluteRect]; 265 | var rulerDeltaX = [absrect rulerX] - [absrect x], 266 | rulerDeltaY = [absrect rulerY] - [absrect y], 267 | CGRectRulerX = cgrect.origin.x + rulerDeltaX, 268 | CGRectRulerY = cgrect.origin.y + rulerDeltaY; 269 | return { 270 | x: Math.round(CGRectRulerX), 271 | y: Math.round(CGRectRulerY) 272 | } 273 | } 274 | 275 | // 276 | // Helpers 277 | // 278 | 279 | var combobox = function(msg, items, selectedItemIndex){ 280 | selectedItemIndex = selectedItemIndex || 0; 281 | 282 | var combobox = [[NSComboBox alloc] initWithFrame:NSMakeRect(0,0,50,25)]; 283 | [combobox addItemsWithObjectValues:items]; 284 | [combobox selectItemAtIndex:selectedItemIndex]; 285 | 286 | var alert = [[NSAlert alloc] init]; 287 | [alert setMessageText:msg]; 288 | [alert addButtonWithTitle:'Save']; 289 | [alert addButtonWithTitle:'Cancel']; 290 | [alert setAccessoryView:combobox]; 291 | 292 | var responseCode = [alert runModal]; 293 | var combosel = [combobox indexOfSelectedItem]; 294 | var combovalue = [combobox stringValue]; 295 | log(combovalue); 296 | 297 | return [responseCode, combosel, combovalue]; 298 | } 299 | 300 | // 301 | // Layer Helpers 302 | // 303 | 304 | var should_ignore_layer = function(layer) { 305 | return [layer name].slice(-1) == "-"; 306 | } 307 | 308 | var should_flatten_layer = function(layer) { 309 | return [layer name].slice(-1) == "*"; 310 | } 311 | 312 | var should_make_layer_own_image = function(layer) { 313 | return [layer name].slice(-1) == '+'); 314 | } 315 | 316 | var should_export_layer = function(layer) { 317 | return (is_group(layer) || should_make_layer_own_image(layer)) && !should_ignore_layer(layer); 318 | } 319 | 320 | var is_group = function(layer) { 321 | return [layer isMemberOfClass:[MSLayerGroup class]] || [layer isMemberOfClass:[MSArtboardGroup class]] 322 | } 323 | 324 | var is_symbol = function(layer) { 325 | if(layer.parentOrSelfIsSymbol){ 326 | return [layer parentOrSelfIsSymbol]; 327 | } else { 328 | return [layer isKindOfClass:MSSymbolInstance]; 329 | } 330 | } 331 | 332 | // 333 | // File helpers 334 | // 335 | 336 | var sanitize_filename = function(name) { 337 | return name.replace(/(:|\/)/g ,"_").replace(/__/g,"_").replace("*","").replace("+","").replace("@@hidden",""); // TODO: replace spaces? /(\s|:|\/)/g 338 | } 339 | 340 | var check_name_conflict = function(layer, depth, artboard_name) { 341 | var was_conflict = false; 342 | if (array_contains(layer_names, artboard_name + [layer name])) { 343 | was_conflict = true; 344 | log_depth("Layer name conflict: <" + [layer name] + "> already exists in artboard <" + artboard_name + ">", depth); 345 | 346 | // Preserve layer name modifiers + - * 347 | var last_char = [layer name].slice(-1); 348 | var has_modifier = should_ignore_layer(layer) || should_flatten_layer(layer) || should_make_layer_own_image(layer); 349 | if (has_modifier) { 350 | [layer setName:[layer name].split(last_char)[0]]; // Remove last_char 351 | } 352 | [layer setName:[layer name] + " copy"]; 353 | if (has_modifier) { 354 | [layer setName:[layer name] + last_char]; 355 | } 356 | log_depth("Renaming to: <" + [layer name] + ">", depth); 357 | 358 | check_name_conflict(layer, depth, artboard_name); 359 | } 360 | 361 | return was_conflict; 362 | } 363 | 364 | // 365 | // Debug Helpers 366 | // 367 | 368 | var alert = function(msg, title){ 369 | var app = [NSApplication sharedApplication]; 370 | [app displayDialog:msg withTitle:title]; 371 | } 372 | 373 | log_depth = function(message, depth) { 374 | log_depth_core(message, depth, ">"); 375 | } 376 | 377 | log_depth_core = function(message, depth, spacer) { 378 | var padding = spacer; 379 | for(var i=0; i