├── .github └── FUNDING.yml ├── Anchors ├── Add Caret Anchors.py ├── Copy Layer Anchors from Other Font.py ├── Copy Selected Anchors from Other Font.py └── Re-interpolate Anchors.py ├── Color Fonts ├── SVG Export.py └── SVG Import.py ├── Components ├── Decompose Nested Components.py ├── Make Component Glyph across All Layers.py ├── New Tab with Components in Background.py ├── New Tab with Nested Components.py ├── Rebuild Components in Double Quotes.py ├── Report Components Vertical Position.py └── Set Components Alignment Type 3.py ├── Font ├── Export Fonts into Subfolder.py ├── Remove Vertical Metrics Parameters from Instances.py ├── Reorder Axes.py ├── Replace in Family Name.py ├── Report Windows Names.py └── Sort Instances.py ├── Glyph Names ├── Copy Sort Names.py ├── List Glyphs in Current Tab.py └── Rename Glyphs and Update Features.py ├── Hinting ├── Export Hinting HTML Test Page.py ├── New Tab with Rotated, Scaled or Flipped Components.py ├── New Tab with Vertically Shifted Components.py └── Reverse PS Hint.py ├── Interpolation └── New Tab with Repeating Components and Paths.py ├── LICENSE ├── Layers ├── Copy Master into Sublayer.py └── Remove all layers for the current master.py ├── Metrics and Kerning ├── Copy Kerning Groups from Unsuffixed Glyphs.py ├── New Tab with Kerning Exceptions.py ├── New Tab with Kerning Pairs for Selected Glyph.py ├── New Tab with Missing Kerning Pairs.py ├── New Tab with Zero Kerning Pairs.py └── Round All Kerning.py ├── Paths ├── Add Extremes to Selection.py ├── Add Node at 45 degrees.py ├── Add Point Along Segment.py ├── Create Centerline.py ├── Duplicate Selected Nodes with Offcurve Points.py ├── Duplicate Selected Nodes.py ├── Interpolate Path with Itself.py ├── Make Block Shadow.py ├── Make Next Node First.py ├── Make Previous Node First.py ├── New Tab with Overlaps.py ├── Open All Nodes.py ├── Open Selected Nodes.py ├── Re-interpolate.py ├── Remove Overlaps and Correct Path Directions.py ├── Round Coordinates for All Layers.py └── Round Coordinates for Entire Font.py └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: harbortype -------------------------------------------------------------------------------- /Anchors/Add Caret Anchors.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Caret Anchors 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds caret_* anchors to the selected glyphs based from the base glyphs’ widths. Applies to all layers. Does not modify existing caret anchors. 6 | """ 7 | 8 | from AppKit import NSPoint 9 | from math import tan, radians 10 | from GlyphsApp import Glyphs, GSAnchor 11 | 12 | Glyphs.clearLog() 13 | 14 | this_font = Glyphs.font 15 | 16 | glyph_components = {} 17 | selected_glyphs = [lyr.parent for lyr in this_font.selectedLayers] 18 | 19 | 20 | def get_spacing_components(this_layer): 21 | """Get a list of the spacing components.""" 22 | spacing_components = [] 23 | for this_component in this_layer.components: 24 | parent_glyph = this_component.component 25 | if parent_glyph.subCategory == "Nonspacing": 26 | continue 27 | spacing_components.append(this_component) 28 | return spacing_components 29 | 30 | 31 | for this_glyph in selected_glyphs: 32 | 33 | # Figure out if the glyph has a dot suffix for ligatures 34 | suffix = None 35 | possible_suffixes = [".liga", ".dlig", ".rlig"] 36 | for sufx in possible_suffixes: 37 | if sufx in this_glyph.name: 38 | suffix = sufx 39 | break 40 | 41 | for this_layer in this_glyph.layers: 42 | 43 | layer_id = this_layer.layerId 44 | x_height = this_font.masters[layer_id].xHeight 45 | italic_angle = this_font.masters[layer_id].italicAngle 46 | italic_offset = tan(radians(italic_angle)) * (x_height / 2) 47 | 48 | layer_components = get_spacing_components(this_layer) 49 | layer_anchors = [anchor.name for anchor in this_layer.anchors] 50 | 51 | if layer_components and not this_layer.paths: 52 | for i, this_component in enumerate(layer_components): 53 | if i == 0: 54 | continue # Skip the first component 55 | offset = this_component.position.x 56 | new_anchor_name = "caret_" + str(i) 57 | if new_anchor_name not in layer_anchors: 58 | new_anchor = GSAnchor( 59 | new_anchor_name, 60 | NSPoint(round(offset - italic_offset), 0) 61 | ) 62 | this_layer.anchors.append(new_anchor) 63 | -------------------------------------------------------------------------------- /Anchors/Copy Layer Anchors from Other Font.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Layer Anchors from Other Font 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Copies the anchors of the current layer from the background file. 6 | """ 7 | 8 | this_font = Glyphs.font 9 | other_font = Glyphs.fonts[1] 10 | 11 | 12 | def CopyAnchors(origin_layer, target_layer): 13 | print(origin_layer, target_layer) 14 | target_layer.anchors = [] 15 | for this_anchor in origin_layer.anchors: 16 | target_layer.anchors.append(this_anchor.copy()) 17 | 18 | try: 19 | this_font.disableUpdateInterface() 20 | 21 | master_index = this_font.masters.index(this_font.selectedFontMaster) 22 | for this_layer in this_font.selectedLayers: 23 | this_glyph = this_layer.parent 24 | if this_glyph.name not in other_font.glyphs: 25 | print(f"{this_glyph.name} does not exist in background font.") 26 | continue 27 | origin_glyph = other_font.glyphs[this_glyph.name] 28 | origin_layer = origin_glyph.layers[master_index] 29 | target_layer = this_glyph.layers[master_index] 30 | CopyAnchors(origin_layer, target_layer) 31 | 32 | finally: 33 | this_font.enableUpdateInterface() 34 | -------------------------------------------------------------------------------- /Anchors/Copy Selected Anchors from Other Font.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Selected Anchors from Other Font 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Copies the position of the selected anchors from the background file. 6 | """ 7 | 8 | this_font = Glyphs.font 9 | other_font = Glyphs.fonts[1] 10 | 11 | font_master = this_font.selectedFontMaster 12 | master_index = this_font.masters.index(font_master) 13 | 14 | for this_layer in this_font.selectedLayers: 15 | this_glyph = this_layer.parent 16 | other_glyph = other_font.glyphs[this_glyph.name] 17 | other_layer = other_glyph.layers[master_index] 18 | for this_anchor in this_layer.anchors: 19 | if this_anchor in this_layer.selection: 20 | other_anchor = other_layer.anchors[this_anchor.name] 21 | this_anchor.position = other_anchor.position 22 | -------------------------------------------------------------------------------- /Anchors/Re-interpolate Anchors.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Re-interpolate Anchors 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Re-interpolates only the anchors on selected layers. 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | thisFont = Glyphs.font 13 | 14 | for thisLayer in thisFont.selectedLayers: 15 | # create a temporary copy of the current layer 16 | originalLayer = thisLayer.copy() 17 | # reinterpolate the layer 18 | thisLayer.reinterpolate() 19 | # put back paths and components as of before reinterpolating the layer 20 | try: # Glyphs 3 21 | thisLayer.shapes = originalLayer.shapes 22 | except: # Glyphs 2 23 | thisLayer.paths = originalLayer.paths 24 | thisLayer.components = originalLayer.components 25 | thisLayer.width = originalLayer.width 26 | -------------------------------------------------------------------------------- /Color Fonts/SVG Export.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: SVG Export 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Export SVG files to a subfolder defined by the user. The script takes a JSON file as input. In the JSON file, define the layers which will be exported (from the bottom up) and its colors. 8 | """ 9 | 10 | from drawBot import fill, linearGradient, newDrawing, newPage, drawPath, saveImage 11 | import vanilla 12 | import os 13 | import re 14 | import json 15 | from collections import OrderedDict 16 | from GlyphsApp import Glyphs 17 | 18 | 19 | class ExportSVG(object): 20 | 21 | def __init__(self): 22 | winWidth = 240 23 | winHeight = 110 24 | self.w = vanilla.Window( 25 | # Define the window size, title, min size, max size, 26 | # and last position and size 27 | (winWidth, winHeight), 28 | "Export SVG", # window title 29 | minSize=(winWidth, winHeight), 30 | maxSize=(winWidth, winHeight), 31 | autosaveName="com.harbortype.ExportSvg.mainwindow" 32 | ) 33 | self.w.text_1 = vanilla.TextBox( 34 | (15, 20, 100, 17), 35 | "Layer name:", 36 | sizeStyle='regular' 37 | ) 38 | self.w.exportName = vanilla.EditText( 39 | (100, 18, 120, 22), 40 | "svg", 41 | sizeStyle='regular' 42 | ) 43 | self.w.runButton = vanilla.Button( 44 | (-100, -35, -20, -15), 45 | "Export", 46 | sizeStyle="regular", 47 | callback=self.main 48 | ) 49 | self.w.setDefaultButton(self.w.runButton) 50 | 51 | # Load Settings: 52 | if not self.loadPreferences(): 53 | print("Note: 'Export SVG' could not load preferences. Will resort to defaults") 54 | 55 | # Open window and focus on it: 56 | self.w.open() 57 | self.w.makeKey() 58 | 59 | def savePreferences(self, sender): 60 | try: 61 | Glyphs.defaults["com.harbortype.ExportSVG.exportName"] = self.w.exportName.get() 62 | except: 63 | return False 64 | return True 65 | 66 | def loadPreferences(self): 67 | try: 68 | Glyphs.registerDefault("com.harbortype.ExportSVG.unicode", "svg") 69 | self.w.exportName.set( 70 | Glyphs.defaults["com.harbortype.ExportSVG.exportName"] 71 | ) 72 | except: 73 | return False 74 | return True 75 | 76 | def readJSON(self, jsonFile): 77 | """ 78 | Reads an JSON file and return its contents as an OrderedDict 79 | 80 | Arguments: 81 | jsonFile {str} -- complete path for .json file 82 | Returns: 83 | OrderedDict 84 | """ 85 | # jsonFile = os.path.join(filePath, fileName, '.json') 86 | try: 87 | with open(jsonFile) as data_file: 88 | jsonData = json.load(data_file, object_pairs_hook=OrderedDict) 89 | return jsonData 90 | except: 91 | Glyphs.showMacroWindow() 92 | print('No JSON file exists at %s' % (jsonFile)) 93 | 94 | def setColors(self, layerColors): 95 | """ 96 | Gets a list or OrderedDict of colors, converts them to floatpoint 97 | notation (from 0.0 to 1.0 instead of 0 to 255), and then declares the 98 | fill or linearGradient funcions 99 | 100 | Arguments: 101 | layerColors {list or OrderedDict} -- coming from a JSON file 102 | """ 103 | if type(layerColors) is list: 104 | # If layerColors is a list, it means it is a solid fill 105 | for i, value in enumerate(layerColors): 106 | # Convert each rgb value from 0-255 to 0.0-1.0 formats 107 | if type(value) is int and i <= 2: 108 | # Only process if it hasn't already been processed 109 | # (not int) and is one of the first 3 values (rgb) 110 | layerColors[i] = value / 255 111 | # Convert the processed values into a tuple, 112 | # then unpack it and set it as fill color 113 | fillColor = tuple(layerColors) 114 | fill(*fillColor) 115 | 116 | elif type(layerColors) is OrderedDict: 117 | # If layerColors is an OrderedDict, it means it is a gradient 118 | for i, stop in enumerate(layerColors['colors']): 119 | # For each color stop in the gradient 120 | for v, value in enumerate(stop): 121 | # For each color value in the color stop 122 | if type(value) is int and v <= 2: 123 | # Only process if it hasn't already been processed 124 | # (not int) and is one of the first 3 values (rgb) 125 | layerColors['colors'][i][v] = value / 255 126 | # Convert the processed values into a list, then use them to 127 | # define a linearGradient. We are explicitly declaring the 128 | # arguments because Python 2 only allows to unpack the last 129 | # tuple in an argument list. 130 | colors = list(layerColors['colors']) 131 | locations = layerColors['locations'] 132 | linearGradient( 133 | startPoint=layerColors['startPoint'], 134 | endPoint=layerColors['endPoint'], 135 | locations=locations, 136 | colors=colors 137 | ) 138 | 139 | def regexSvg(self, svgPath): 140 | """ 141 | Perform some color substitutions on the SVG file to make it 142 | compatible with Adobe software, as they don't support rgba notation 143 | 144 | Arguments: 145 | svgPath {str} -- path of the SVG file 146 | """ 147 | 148 | # Lê o conteúdo do arquivo svg e guarda em uma variável 149 | with open(svgPath) as f: 150 | svgFile = f.read() 151 | 152 | # Substitui rgba(000,000,000,1.0) 153 | # por rgb(000,000,000) 154 | svgFile = re.sub( 155 | r'rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),1\.0\)', 156 | r'rgb(\1,\2,\3)', 157 | svgFile) 158 | 159 | # Substitui stop-color="rgba(000,000,000,0.0)" 160 | # por stop-color="rgb(000,000,000)" stop-opacity="0.0" 161 | svgFile = re.sub( 162 | r'stop-color="rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(0?\.\d+)\)"', 163 | r'stop-color="rgb(\1,\2,\3)" stop-opacity="\4"', 164 | svgFile) 165 | 166 | # Substitui fill="rgba(000,000,000,0.0)" 167 | # por fill="rgb(000,000,000)" fill-opacity="0.0" 168 | svgFile = re.sub( 169 | r'fill="rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(0?\.\d+)\)"', 170 | r'fill="rgb(\1,\2,\3)" fill-opacity="\4"', 171 | svgFile) 172 | 173 | with open(svgPath, 'w') as f: 174 | f.write(svgFile) 175 | 176 | def main(self, sender): 177 | try: 178 | # Basic variables 179 | myFont = Glyphs.font 180 | upm = myFont.upm 181 | fontPath = os.path.dirname(myFont.filepath) 182 | exportName = self.w.exportName.get() 183 | outputFolderPath = os.path.join(fontPath, exportName) 184 | outputSubfolderPath = '_other' 185 | 186 | # Calls the readJSON function using the jsonPath and returns the 187 | # jsonData as an OrderedDict. The JSON files contain the layer 188 | # names and their colors. 189 | jsonPath = os.path.join(fontPath, exportName + '.json') 190 | jsonData = self.readJSON(jsonPath) 191 | # Gets the jsonData keys (the layer names) which will be used 192 | # later on for iteration 193 | layerNames = list(jsonData.keys()) 194 | 195 | # Creates an empty list and populates it with all glyph names. 196 | allGlyphNames = [] 197 | for glyph in myFont.glyphs: 198 | allGlyphNames.append(glyph.name) 199 | # Creates an empty set and populates it with unique glyph names 200 | # (case insensitive). Glyphs with duplicate names are then stored 201 | # on a separate list, which will later be used to decide if any 202 | # glyph should be stored at root or in a subfolder 203 | uniqueNames = set() 204 | glyphsToSubfolder = [] 205 | for glyphName in allGlyphNames: 206 | # Checks if the glyph name is already in the uniqueNames set. 207 | # If positive, stores the glyph name on the other list 208 | if glyphName.lower() in uniqueNames: 209 | glyphsToSubfolder.append(glyphName) 210 | # Stores the glyph name in the uniqueNames set 211 | uniqueNames.add(glyphName.lower()) 212 | 213 | # Checks if there are glyph that should be stored on a subfolder. 214 | # If true, checks if the subfolder exists or creates it. 215 | if len(glyphsToSubfolder) > 0: 216 | nestDir = os.path.join(outputFolderPath, outputSubfolderPath) 217 | if not os.path.exists(nestDir): 218 | os.makedirs(nestDir) 219 | 220 | # MAIN LOOP 221 | for glyph in myFont.glyphs: 222 | 223 | # Defines where the SVG will be stored 224 | if glyph.name in glyphsToSubfolder: 225 | outputPath = nestDir 226 | else: 227 | outputPath = outputFolderPath 228 | 229 | newDrawing() 230 | newPage(upm, upm) 231 | 232 | glyphName = glyph.name 233 | 234 | for i, layerName in enumerate(layerNames): 235 | for master in myFont.masters: 236 | if master.name == layerName: 237 | masterId = master.id 238 | break 239 | 240 | layerColors = jsonData[layerName] 241 | self.setColors(layerColors) 242 | drawPath(glyph.layers[masterId].completeBezierPath) 243 | 244 | svgPath = os.path.join(outputPath, glyphName + '.svg') 245 | saveImage(svgPath) 246 | self.regexSvg(svgPath) 247 | 248 | if not self.savePreferences(self): 249 | print("Note: 'Export SVG' could not write preferences.") 250 | 251 | self.w.close() 252 | 253 | print('Done!') 254 | 255 | except Exception as e: 256 | # brings macro window to front and reports error: 257 | Glyphs.showMacroWindow() 258 | print("Export SVG Error:\n%s" % e) 259 | 260 | 261 | ExportSVG() 262 | -------------------------------------------------------------------------------- /Color Fonts/SVG Import.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: SVG Import 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Import SVG files to the 'svg' layer on each glyph. Place the SVG files in a subfolder with the name of your master. If more than one master is present, it will search for each one of them. 8 | """ 9 | 10 | import os 11 | from GlyphsApp import Glyphs, GSLayer, GSBackgroundImage 12 | 13 | # Glyphs.clearLog() 14 | # Glyphs.showMacroWindow() 15 | 16 | # Basic variables 17 | myFont = Glyphs.font 18 | upm = myFont.upm 19 | 20 | # Default folder is the current folder 21 | currentDir = os.path.dirname(myFont.filepath) 22 | 23 | 24 | def findFolder(currentMaster): 25 | """ 26 | Reads the name of the current master and checks if there is a subfolder 27 | with the same name (relative to the frontmost .glyphs file). 28 | The comparison is case insensitive. 29 | 30 | Arguments: 31 | currentMaster {str} -- GSMaster object 32 | 33 | Returns: valid subfolder path 34 | """ 35 | masterName = currentMaster.name 36 | potentialSubfolder = os.path.join(currentDir, masterName) 37 | 38 | if os.path.isdir(potentialSubfolder): 39 | return potentialSubfolder 40 | else: 41 | return None 42 | 43 | 44 | def findSvgLayer(layers, masterId): 45 | """ 46 | Finds the 'svg' layer associated with a specific master ID 47 | 48 | Arguments: 49 | layers {arr} -- array of GSLayers of a glyph 50 | masterId {str} -- unique ID of master 51 | 52 | Returns: layer object 53 | """ 54 | for layer in layers: 55 | # Find the svg layer associated with the current master 56 | if layer.name == 'svg' and layer.associatedMasterId == masterId: 57 | return layer 58 | return None 59 | 60 | 61 | myFont.disableUpdateInterface() 62 | 63 | for master in myFont.masters: 64 | # Start main loop 65 | # For each master, assign the findFolder result to the svgFolder variable 66 | # and store its ID on masterId 67 | svgFolder = findFolder(master) 68 | masterId = master.id 69 | 70 | if svgFolder: 71 | # If path exists, creates an empty dictionary which will be used to 72 | # store glyph names and svg paths 73 | glyphNamesDict = {} 74 | 75 | for root, dirs, files in os.walk(svgFolder): 76 | # Filter the files list, keeping svg files only 77 | files = [fi for fi in files if fi.endswith(".svg")] 78 | 79 | for fileName in files: 80 | # For each file in dir, store the glyph name and the 81 | # complete svg path in the dictionaty 82 | glyphName = fileName[:-4] 83 | glyphNamesDict[glyphName] = os.path.join(root.lower(), fileName) 84 | 85 | # The dictionary is complete now. 86 | # Let's start adding the svg files to the font 87 | 88 | for glyphName, svgPath in glyphNamesDict.iteritems(): 89 | # We iterate svg files instead of glyphs in the font 90 | # because it is more likely to exist fewer svgs than glyphs 91 | g = myFont.glyphs[glyphName] 92 | 93 | if g: 94 | # If a glyph exists with the same name as the svg, 95 | # reset the svgLayer variable 96 | svgLayer = None 97 | svgLayer = findSvgLayer(g.layers, masterId) 98 | if svgLayer: 99 | # Clears the svg layer if it exists and is a 100 | # child of the current master 101 | svgLayer.backgroundImage = None 102 | svgLayer.paths = None 103 | svgLayer.anchors = None 104 | else: 105 | # If the svg layer does not exists, create it 106 | newLayer = GSLayer() 107 | newLayer.name = 'svg' 108 | newLayer.associatedMasterId = masterId 109 | g.layers.append(newLayer) 110 | # Run the function to find the svg layer again so we can 111 | # select the freshly created layer 112 | svgLayer = findSvgLayer(g.layers, masterId) 113 | 114 | # Add the image to the svg layer 115 | newImage = GSBackgroundImage.alloc().initWithPath_(svgPath) 116 | svgLayer.setBackgroundImage_(newImage) 117 | 118 | myFont.enableUpdateInterface() 119 | -------------------------------------------------------------------------------- /Components/Decompose Nested Components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Decompose Nested Components 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Decompose nested components on selected glyphs. 8 | """ 9 | from Foundation import NSPoint 10 | from GlyphsApp import Glyphs, Message 11 | 12 | thisFont = Glyphs.font 13 | decomposed = [] 14 | 15 | 16 | def hasExportingComponents(glyph): 17 | for comp in glyph.layers[0].components: 18 | if comp.component.export: 19 | return True 20 | return False 21 | 22 | 23 | for layer in thisFont.selectedLayers: 24 | thisGlyph = layer.parent 25 | for thisLayer in thisGlyph.layers: 26 | if not thisLayer.isMasterLayer and not thisLayer.isSpecialLayer: 27 | continue 28 | toDecompose = [] 29 | for i in range(len(thisLayer.components) - 1, -1, -1): 30 | thisComponent = thisLayer.components[i] 31 | thisComponentPosition = thisComponent.position 32 | otherGlyph = thisFont.glyphs[thisComponent.name] 33 | if otherGlyph.layers[0].components: 34 | if hasExportingComponents(otherGlyph): 35 | otherGlyphPosition = otherGlyph.layers[thisLayer.layerId].components[0].position 36 | if thisGlyph.name not in decomposed: 37 | decomposed.append(thisGlyph.name) 38 | thisComponent.decompose(doAnchors=False) 39 | # Reposition component 40 | newComponent = thisLayer.components[i] 41 | if newComponent.position.y: 42 | newComponent.automaticAlignment = False 43 | newComponent.position.x = NSPoint( 44 | thisComponentPosition.x + otherGlyphPosition.x, 45 | thisComponentPosition.y + otherGlyphPosition.y 46 | ) 47 | 48 | 49 | if len(decomposed): 50 | Glyphs.font.newTab("/" + "/".join(decomposed)) 51 | else: 52 | Message( 53 | title="Decompose Nested Components", 54 | message="There are no components of components in the selected glyphs.", 55 | ) 56 | -------------------------------------------------------------------------------- /Components/Make Component Glyph across All Layers.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make Component Glyph across All Layers 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Assembles the selected glyphs from components (the same as running the Make Component Glyph command for all layers) and removes all anchors. 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | font = Glyphs.font 13 | for selectedLayer in font.selectedLayers: 14 | glyph = selectedLayer.parent 15 | for layer in glyph.layers: 16 | layer.makeComponents() 17 | layer.anchors = [] 18 | -------------------------------------------------------------------------------- /Components/New Tab with Components in Background.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Components in Background 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Opens a new Edit tab with glyphs containing components in their backgrounds. 7 | """ 8 | 9 | from GlyphsApp import Glyphs, GSControlLayer, Message 10 | 11 | thisFont = Glyphs.font 12 | tabLayers = [] 13 | 14 | for thisGlyph in thisFont.glyphs: 15 | glyphHasComponents = False 16 | for thisLayer in thisGlyph.layers: 17 | for thisComponent in thisLayer.background.components: 18 | tabLayers.append(thisLayer) 19 | glyphHasComponents = True 20 | break 21 | if glyphHasComponents: 22 | tabLayers.append(GSControlLayer(10)) 23 | 24 | 25 | if tabLayers: 26 | thisFont.newTab() 27 | thisFont.tabs[-1].layers = tabLayers 28 | else: 29 | Message( 30 | title="New Tab with Components in Background", 31 | message="There are no components in any glyph background.", 32 | OKButton="OK", 33 | ) 34 | -------------------------------------------------------------------------------- /Components/New Tab with Nested Components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Nested Components 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Opens a new Edit tab with glyphs that contain components made of components. 7 | """ 8 | 9 | from GlyphsApp import Glyphs, Message 10 | 11 | thisFont = Glyphs.font 12 | txt = "" 13 | 14 | 15 | def hasExportingComponents(glyph): 16 | for comp in glyph.layers[0].components: 17 | if comp.component.export: 18 | return True 19 | return False 20 | 21 | 22 | def hasOnlySmartComponents(components): 23 | for component in components: 24 | if not component.componentName.startswith("_smart."): 25 | return False 26 | return True 27 | 28 | 29 | for thisGlyph in thisFont.glyphs: 30 | if not thisGlyph.export: 31 | continue 32 | firstLayer = thisGlyph.layers[0] 33 | for thisComponent in firstLayer.components: 34 | otherGlyph = thisFont.glyphs[thisComponent.name] 35 | if otherGlyph.layers[0].components: 36 | if hasExportingComponents(otherGlyph): 37 | txt += "/{0}".format(thisGlyph.name) 38 | break 39 | 40 | if txt: 41 | Glyphs.font.newTab(txt) 42 | else: 43 | Message( 44 | title="New Tab with Nested Components", 45 | message="There are no components of components in this font.", 46 | OKButton="OK", 47 | ) 48 | -------------------------------------------------------------------------------- /Components/Rebuild Components in Double Quotes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Rebuild Components in Double Quotes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Replaces components in double quotes using the single quotes in all layers. For example, if the quotedblleft is made from a rotated quotedblright, it will copy the current component to the background and rebuild it using 2 quotelefts. 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | Glyphs.clearLog() 12 | 13 | font = Glyphs.font 14 | 15 | quotes = { 16 | "quoteleft": ["quotedblleft"], 17 | "quoteright": ["quotedblright"], 18 | "quotesinglbase": ["quotedblbase"], 19 | } 20 | 21 | font.disableUpdateInterface() 22 | 23 | for quote in quotes.items(): 24 | base_glyph = quote[0] 25 | compound_glyphs = quote[1] 26 | print(compound_glyphs) 27 | 28 | if base_glyph not in font.glyphs: 29 | print("🚫 {} not in font.".format(base_glyph)) 30 | continue 31 | 32 | for layer in font.glyphs[base_glyph].layers: 33 | layer.decomposeComponents() 34 | 35 | for glyph in compound_glyphs: 36 | if glyph not in font.glyphs: 37 | print("🚫 {} not in font.".format(glyph)) 38 | continue 39 | for layer in font.glyphs[glyph].layers: 40 | layer.background = layer.copyDecomposedLayer() 41 | for component in layer.components: 42 | original_bounds = component.bounds 43 | component.name = base_glyph 44 | component.rotation = 0.0 45 | new_bounds = component.bounds 46 | delta_x = original_bounds.origin.x - new_bounds.origin.x 47 | component.applyTransform(( 48 | 1.0, # scale x 49 | 0.0, # skew x 50 | 0.0, # skew y 51 | 1.0, # scale y 52 | delta_x, # position x 53 | 0.0, # position y 54 | )) 55 | background_bounds = layer.background.bounds 56 | new_bounds = layer.bounds 57 | delta_y = background_bounds.origin.y - new_bounds.origin.y 58 | layer.applyTransform(( 59 | 1.0, # scale x 60 | 0.0, # skew x 61 | 0.0, # skew y 62 | 1.0, # scale y 63 | 0.0, # position x 64 | delta_y, # position y 65 | )) 66 | print("✅ Replaced components in {}.".format(glyph)) 67 | 68 | font.enableUpdateInterface() 69 | 70 | Glyphs.showMacroWindow() 71 | -------------------------------------------------------------------------------- /Components/Report Components Vertical Position.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Report Components Vertical Position 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Reports the y coordinate of all components in the selected glyphs. 6 | """ 7 | 8 | from Foundation import NSPoint 9 | from GlyphsApp import Glyphs 10 | 11 | Glyphs.clearLog() 12 | 13 | this_font = Glyphs.font 14 | 15 | all_positions = {} 16 | for this_layer in this_font.selectedLayers: 17 | components_coords = [] 18 | for this_component in this_layer.components: 19 | components_coords.append(this_component.position.y) 20 | all_positions[this_layer.parent.name] = tuple(components_coords) 21 | 22 | for glyph_name, positions in all_positions.items(): 23 | print(positions, glyph_name) 24 | 25 | only_positions = list(all_positions.values()) 26 | if len(list(set(list(all_positions.values())))) == 1: 27 | print("✅ All good") 28 | else: 29 | print("⚠️ Different positions:", set(only_positions)) 30 | 31 | Glyphs.showMacroWindow() 32 | -------------------------------------------------------------------------------- /Components/Set Components Alignment Type 3.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set Components Alignment Type 3 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Sets the automatic alignment of all components in the selected glyphs to be type 3 (the type that allows to be vertically shifted). Applies to all masters. 7 | """ 8 | 9 | from GlyphsApp import Glyphs, GSShapeTypeComponent 10 | 11 | this_font = Glyphs.font 12 | 13 | for this_layer in this_font.selectedLayers: 14 | this_glyph = this_layer.parent 15 | for glyph_layer in this_glyph.layers: 16 | for this_shape in glyph_layer.shapes: 17 | if this_shape.shapeType != GSShapeTypeComponent: 18 | continue 19 | this_shape.alignment = 3 20 | -------------------------------------------------------------------------------- /Font/Export Fonts into Subfolder.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Export Fonts into Subfolder 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Exports OTF and TTF at the same time into a specified subfolder. Needs Vanilla. 8 | """ 9 | 10 | import os 11 | import errno 12 | import vanilla 13 | from AppKit import NSAttributedString, NSColor, NSForegroundColorAttributeName, NSUserDefaults 14 | from GlyphsApp import Glyphs 15 | 16 | Glyphs.clearLog() 17 | 18 | 19 | class exportOtfTtf(object): 20 | 21 | windowWidth = 400 22 | windowHeight = 320 23 | margin = 25 24 | line = 22 25 | currentLine = 0 26 | column = (windowWidth - margin * 4) / 2 27 | 28 | attributes = { 29 | NSForegroundColorAttributeName: NSColor.colorWithCalibratedRed_green_blue_alpha_(0, 0, 0, .3), 30 | } 31 | 32 | def __init__(self): 33 | 34 | self.w = vanilla.Window( 35 | (self.windowWidth, self.windowHeight), 36 | "Export OTF and TTF into Subfolder", 37 | minSize=(self.windowWidth, self.windowHeight), 38 | maxSize=(self.windowWidth, self.windowHeight), 39 | autosaveName="com.harbortype.exportOtfTtf.mainwindow" 40 | ) 41 | 42 | fontName = NSAttributedString.alloc().initWithString_attributes_( 43 | "File: " + os.path.basename(Glyphs.font.filepath), 44 | self.attributes 45 | ) 46 | self.w.currentFont = vanilla.TextBox( 47 | (self.margin, self.margin - 10, -self.margin, self.line), 48 | fontName, 49 | alignment="right" 50 | ) 51 | self.currentLine += self.line 52 | 53 | self.w.subfolder_title = vanilla.TextBox( 54 | (self.margin, self.margin + self.currentLine, 80, self.line), 55 | "Subfolder: " 56 | ) 57 | self.w.subfolder = vanilla.EditText( 58 | (self.margin + 80, self.margin + self.currentLine - 3, -self.margin, self.line), 59 | ) 60 | self.currentLine += self.line + 13 61 | 62 | self.w.allOpenFonts = vanilla.CheckBox( 63 | (self.margin, self.margin + self.currentLine, self.column, self.line), 64 | "All open fonts", 65 | callback=self.savePreferences 66 | ) 67 | 68 | self.currentLine += self.line + 13 69 | 70 | self.w.line = vanilla.VerticalLine( 71 | (self.windowWidth / 2, self.margin + self.currentLine, 1, self.line * 3) 72 | ) 73 | 74 | self.w.otf = vanilla.CheckBox( 75 | (self.margin, self.margin + self.currentLine, self.column, self.line), 76 | "Export OTF", 77 | callback=self.savePreferences 78 | ) 79 | self.currentLine += self.line 80 | self.w.otfRemoveOverlaps = vanilla.CheckBox( 81 | (self.margin * 2, self.margin + self.currentLine, self.column, self.line), 82 | "Remove overlaps", 83 | callback=self.savePreferences 84 | ) 85 | self.currentLine += self.line 86 | self.w.otfAutohint = vanilla.CheckBox( 87 | (self.margin * 2, self.margin + self.currentLine, self.column, self.line), 88 | "Autohint", 89 | callback=self.savePreferences 90 | ) 91 | self.currentLine -= self.line * 2 92 | 93 | self.w.ttf = vanilla.CheckBox( 94 | (self.margin * 3 + self.column, self.margin + self.currentLine, self.column, self.line), 95 | "Export TTF", 96 | callback=self.savePreferences 97 | ) 98 | self.currentLine += self.line 99 | self.w.ttfRemoveOverlaps = vanilla.CheckBox( 100 | (self.margin * 4 + self.column, self.margin + self.currentLine, self.column, self.line), 101 | "Remove overlaps", 102 | callback=self.savePreferences 103 | ) 104 | self.currentLine += self.line 105 | self.w.ttfAutohint = vanilla.CheckBox( 106 | (self.margin * 4 + self.column, self.margin + self.currentLine, self.column, self.line), 107 | "Autohint", 108 | callback=self.savePreferences 109 | ) 110 | self.currentLine += self.line * 1.5 111 | 112 | self.w.progress = vanilla.ProgressBar( 113 | (self.margin, self.margin + self.currentLine, self.windowWidth - self.margin * 2, 16), 114 | ) 115 | self.w.progress.set(0) # set progress indicator to zero 116 | 117 | self.w.closeButton = vanilla.Button( 118 | (self.margin, -self.margin - self.line, self.windowWidth - self.margin * 2, self.line), 119 | "Cancel", 120 | callback=self.closeWindow 121 | ) 122 | self.w.runButton = vanilla.Button( 123 | (self.margin, -self.margin - self.line * 2 - 8, self.windowWidth - self.margin * 2, self.line), 124 | "Export fonts", 125 | callback=self.export 126 | ) 127 | 128 | if not self.loadPreferences(): 129 | print("Note: Could not load preferences. Will resort to defaults.") 130 | 131 | self.w.setDefaultButton(self.w.runButton) 132 | try: 133 | # Python 3 134 | self.w.closeButton.bind(chr(27), []) 135 | except: 136 | # Python 2 137 | self.w.closeButton.bind(unichr(27), []) 138 | self.w.open() 139 | self.w.makeKey() 140 | 141 | def closeWindow(self, sender): 142 | self.w.close() 143 | 144 | def savePreferences(self, sender): 145 | try: 146 | Glyphs.defaults["com.harbortype.exportOtfTtf.subfolder"] = self.w.subfolder.get() 147 | Glyphs.defaults["com.harbortype.exportOtfTtf.otf"] = self.w.otf.get() 148 | Glyphs.defaults["com.harbortype.exportOtfTtf.otfRemoveOverlaps"] = self.w.otfRemoveOverlaps.get() 149 | Glyphs.defaults["com.harbortype.exportOtfTtf.otfAutohint"] = self.w.otfAutohint.get() 150 | Glyphs.defaults["com.harbortype.exportOtfTtf.ttf"] = self.w.ttf.get() 151 | Glyphs.defaults["com.harbortype.exportOtfTtf.ttfRemoveOverlaps"] = self.w.ttfRemoveOverlaps.get() 152 | Glyphs.defaults["com.harbortype.exportOtfTtf.ttfAutohint"] = self.w.ttfAutohint.get() 153 | except: 154 | return False 155 | 156 | return True 157 | 158 | def loadPreferences(self): 159 | try: 160 | NSUserDefaults.standardUserDefaults().registerDefaults_({ 161 | "com.harbortype.exportOtfTtf.subfolder": "fonts", 162 | "com.harbortype.exportOtfTtf.otf": 1, 163 | "com.harbortype.exportOtfTtf.otfRemoveOverlaps": 1, 164 | "com.harbortype.exportOtfTtf.otfAutohint": 1, 165 | "com.harbortype.exportOtfTtf.ttf": 1, 166 | "com.harbortype.exportOtfTtf.ttfRemoveOverlaps": 1, 167 | "com.harbortype.exportOtfTtf.ttfAutohint": 0, 168 | }) 169 | self.w.subfolder.set(Glyphs.defaults["com.harbortype.exportOtfTtf.subfolder"]) 170 | self.w.otf.set(Glyphs.defaults["com.harbortype.exportOtfTtf.otf"]) 171 | self.w.otfRemoveOverlaps.set(Glyphs.defaults["com.harbortype.exportOtfTtf.otfRemoveOverlaps"]) 172 | self.w.otfAutohint.set(Glyphs.defaults["com.harbortype.exportOtfTtf.otfAutohint"]) 173 | self.w.ttf.set(Glyphs.defaults["com.harbortype.exportOtfTtf.ttf"]) 174 | self.w.ttfRemoveOverlaps.set(Glyphs.defaults["com.harbortype.exportOtfTtf.ttfRemoveOverlaps"]) 175 | self.w.ttfAutohint.set(Glyphs.defaults["com.harbortype.exportOtfTtf.ttfAutohint"]) 176 | except: 177 | return False 178 | return True 179 | 180 | def export(self, sender): 181 | self.savePreferences(sender) 182 | otfExport = bool(self.w.otf.get()) 183 | otfRemoveOverlaps = bool(self.w.otfRemoveOverlaps.get()) 184 | otfAutohint = bool(self.w.otfAutohint.get()) 185 | ttfExport = bool(self.w.ttf.get()) 186 | ttfRemoveOverlaps = bool(self.w.ttfRemoveOverlaps.get()) 187 | ttfAutohint = bool(self.w.ttfAutohint.get()) 188 | 189 | formats = { 190 | "otf": { 191 | "export": otfExport, 192 | "removeOverlaps": otfRemoveOverlaps, 193 | "autohint": otfAutohint 194 | }, 195 | "ttf": { 196 | "export": ttfExport, 197 | "removeOverlaps": ttfRemoveOverlaps, 198 | "autohint": ttfAutohint 199 | }, 200 | } 201 | 202 | shouldExport = False 203 | for key in formats.keys(): 204 | if formats[key]["export"]: 205 | shouldExport = True 206 | break 207 | 208 | # Quit if no formats are set to export 209 | if not shouldExport: 210 | print("No files to export!") 211 | Glyphs.showMacroWindow() 212 | quit() 213 | 214 | if self.w.allOpenFonts.get(): 215 | fonts = Glyphs.fonts 216 | else: 217 | fonts = [Glyphs.fonts[0]] 218 | 219 | # Configure the progress bar 220 | formatsCount = 0 221 | for ext in formats: 222 | if formats[ext]["export"]: 223 | formatsCount += 1 224 | totalCount = 0 225 | for font in fonts: 226 | totalCount += len(font.instances) 227 | totalCount = formatsCount * totalCount 228 | 229 | for f, font in enumerate(fonts): 230 | 231 | fontName = NSAttributedString.alloc().initWithString_attributes_( 232 | "[%s/%s] " % (f + 1, len(fonts)) + os.path.basename(Glyphs.font.filepath), 233 | self.attributes 234 | ) 235 | self.w.currentFont.set(fontName) 236 | 237 | # # Configure the progress bar 238 | # formatsCount = 0 239 | # for ext in formats: 240 | # if formats[ext]["export"] == True: 241 | # formatsCount += 1 242 | # totalCount = formatsCount * len(font.instances) 243 | 244 | # Set install folder 245 | subfolder = self.w.subfolder.get() 246 | currentFolder = os.path.dirname(font.filepath) 247 | installFolder = os.path.join(currentFolder, subfolder) 248 | try: 249 | os.makedirs(installFolder) 250 | except OSError as exc: # Python >2.5 251 | if exc.errno == errno.EEXIST and os.path.isdir(installFolder): 252 | pass 253 | else: 254 | raise 255 | 256 | currentCount = 0 257 | count = { 258 | "otf": 0, 259 | "ttf": 0 260 | } 261 | 262 | for ext in formats: 263 | if formats[ext]["export"]: 264 | for instance in font.instances: 265 | try: 266 | if instance.active: 267 | fileName = "%s.%s" % (instance.fontName, ext) 268 | print("Exporting %s" % fileName) 269 | exportStatus = instance.generate( 270 | Format=ext.upper(), 271 | FontPath=installFolder + "/" + fileName, 272 | AutoHint=formats[ext]["autohint"], 273 | RemoveOverlap=formats[ext]["removeOverlaps"] 274 | ) 275 | if exportStatus: 276 | count[ext] += 1 277 | else: 278 | print(exportStatus) 279 | Glyphs.showMacroWindow() 280 | except Exception as e: 281 | print(e) 282 | currentCount += 1 283 | self.w.progress.set(100 / totalCount * currentCount) 284 | 285 | print("Exported %s" % (font.familyName)) 286 | print("%s otf files" % (count["otf"])) 287 | print("%s ttf files" % (count["ttf"])) 288 | Glyphs.showNotification("Exported %s" % (os.path.basename(font.filepath)), "%s otf files\n%s ttf files" % (count["otf"], count["otf"])) 289 | self.w.close() 290 | 291 | 292 | exportOtfTtf() 293 | -------------------------------------------------------------------------------- /Font/Remove Vertical Metrics Parameters from Instances.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Vertical Metrics Parameters from Instances 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Removes all vertical metrics parameters from instances (typo, hhea and win). 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | Glyphs.clearLog() # clears macro window log 13 | font = Glyphs.font 14 | 15 | verticalParameters = [ 16 | "typoAscender", 17 | "typoDescender", 18 | "typoLineGap", 19 | "winAscent", 20 | "winDescent", 21 | "hheaAscender", 22 | "hheaDescender" 23 | ] 24 | count = 0 25 | 26 | for i, instance in enumerate(font.instances): 27 | for p in reversed(range(len(instance.customParameters))): 28 | if instance.customParameters[p].name in verticalParameters: 29 | parameterName = instance.customParameters[p].name 30 | del instance.customParameters[p] 31 | print(instance.name, ": removed %s parameter." % (parameterName)) 32 | count += 1 33 | 34 | if count == 0: 35 | print("No vertical metrics parameters were found in instances. Nothing was removed.") 36 | 37 | Glyphs.showMacroWindow() 38 | -------------------------------------------------------------------------------- /Font/Reorder Axes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reorder Axes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | import vanilla 5 | import collections 6 | from AppKit import NSDragOperationMove 7 | from GlyphsApp import Glyphs 8 | 9 | __doc__ = """ 10 | Reorder axes and their values in masters, instances and special layers. Needs Vanilla. 11 | """ 12 | 13 | genericListPboardType = "genericListPboardType" 14 | Glyphs.clearLog() 15 | 16 | 17 | class ReorderAxes(object): 18 | 19 | thisFont = Glyphs.font # frontmost font 20 | axes = collections.OrderedDict() 21 | for xs in thisFont.axes: 22 | if Glyphs.versionNumber < 3.0: 23 | axes[xs["Tag"]] = xs["Name"] 24 | else: 25 | axes[xs.axisTag] = xs.name 26 | 27 | def __init__(self): 28 | # Window 'self.w': 29 | windowWidth = 280 30 | windowHeight = 190 31 | windowWidthResize = 200 # user can resize width by this value 32 | windowHeightResize = 200 # user can resize height by this value 33 | self.w = vanilla.Window( 34 | (windowWidth, windowHeight), # default window size 35 | "Reorder Axes", # window title 36 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 37 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 38 | autosaveName="com.harbortype.ReorderAxes.mainwindow" # stores last window position and size 39 | ) 40 | 41 | # UI elements: 42 | linePos, inset, lineHeight = 12, 15, 22 43 | # self.w.text_1 = vanilla.TextBox((inset-1, linePos+2, 75, 14), "inset", sizeStyle='small') 44 | # linePos += lineHeight 45 | listValues = [] 46 | for i, (tag, name) in enumerate(self.axes.items()): 47 | listValues.append({"Index": i, "Name": name, "Tag": tag}) 48 | self.w.list_1 = vanilla.List( 49 | (inset, linePos, -inset, -inset - 20 - inset), 50 | listValues, 51 | columnDescriptions=[ 52 | {"title": "Index", "width": 40}, 53 | {"title": "Tag", "width": 60}, 54 | {"title": "Name"}], 55 | allowsMultipleSelection=False, 56 | allowsEmptySelection=True, 57 | dragSettings=dict( 58 | type=genericListPboardType, 59 | operation=NSDragOperationMove, 60 | # allowDropBetweenRows = True, 61 | # allowDropOnRow = False, 62 | callback=self.dragCallback 63 | ), 64 | selfDropSettings=dict( 65 | type=genericListPboardType, 66 | operation=NSDragOperationMove, 67 | allowDropBetweenRows=True, 68 | # allowDropOnRow = False, 69 | callback=self.selfDropCallback 70 | ) 71 | ) 72 | linePos += lineHeight 73 | 74 | # Run Button: 75 | self.w.runButton = vanilla.Button((-80 - inset, -20 - inset, -inset, -inset), "Reorder", sizeStyle='regular', callback=self.Process) 76 | self.w.setDefaultButton(self.w.runButton) 77 | 78 | # Open window and focus on it: 79 | self.w.open() 80 | self.w.makeKey() 81 | 82 | def dragCallback(self, sender, indexes): 83 | self.draggedItems = indexes 84 | 85 | def selfDropCallback(self, sender, dropInfo): 86 | isProposal = dropInfo["isProposal"] 87 | if not isProposal: 88 | target = dropInfo['rowIndex'] 89 | for original in self.draggedItems: 90 | newList = self.w.list_1.get() 91 | if target > original: 92 | newList.insert(target - 1, newList.pop(original)) 93 | else: 94 | newList.insert(target, newList.pop(original)) 95 | self.w.list_1.set(newList) 96 | return True 97 | 98 | def Process(self, sender): 99 | try: 100 | thisFont = Glyphs.font # frontmost font 101 | print("Reorder Axes Report for %s" % thisFont.familyName) 102 | print(thisFont.filepath) 103 | print() 104 | 105 | newOrder = [item["Index"] for item in self.w.list_1.get()] 106 | print("New order is:") 107 | for i in newOrder: 108 | if Glyphs.versionNumber < 3.0: 109 | print(" ", thisFont.axes[i]["Name"]) 110 | else: 111 | print(" ", thisFont.axes[i].name) 112 | print() 113 | 114 | # Reorder the font-wide Axes custom parameter 115 | print("Reordering font axes parameter...") 116 | newAxes = [thisFont.axes[i] for i in newOrder] 117 | thisFont.axes = newAxes 118 | 119 | # Reorder the axis values in each font master 120 | print("Reordering the master's axes values...") 121 | for master in thisFont.masters: 122 | newMasterAxes = [master.axes[i] for i in newOrder] 123 | master.axes = newMasterAxes 124 | 125 | # Reorder the axis values in each instance 126 | print("Reordering the instances's axes values...") 127 | for instance in thisFont.instances: 128 | newInstanceAxes = [instance.axes[i] for i in newOrder] 129 | instance.axes = newInstanceAxes 130 | 131 | # Process special layers 132 | print("Reordering values in special layers...") 133 | for glyph in thisFont.glyphs: 134 | for layer in glyph.layers: 135 | if layer.isSpecialLayer and "{" in layer.name: 136 | first, values = layer.name.split("{") 137 | values = [x.strip() for x in values[:-1].split(",")] 138 | if len(values) != len(newOrder): 139 | print(" ERROR: count of axes in special layer of", glyph.name, "does not match number of axes in font. Skipping...") 140 | continue 141 | newValues = [values[i] for i in newOrder] 142 | newValues = ", ".join(newValues) 143 | layer.name = "%s{%s}" % (first, newValues) 144 | print(" ", glyph.name, "-", layer.name) 145 | 146 | # TODO Process delta hints ??? 147 | # for glyph in thisFont.glyphs: 148 | # for layer in glyph.layers: 149 | # for i in reversed(range(len(layer.hints))): 150 | # hint = layer.hints[i] 151 | # if hint.type == TTDELTA: 152 | # elementDict = hint.elementDict() 153 | # if "settings" in elementDict: 154 | # settings = elementDict["settings"] 155 | # if settings: 156 | # for deltaType in ("deltaH","deltaV"): 157 | # if deltaType in settings: 158 | # newDeltas = {} 159 | # # print(len(settings[deltaType])) 160 | # # print(settings[deltaType]) 161 | # for transformType in settings[deltaType]: 162 | # # print(transformType) 163 | # transformValues = transformType[1:-1] # remove { and } 164 | # transformValues = transformValues.split(", ") 165 | # newValues = [transformValues[i] for i in newOrder] 166 | # while len(newValues) < len(transformValues): 167 | # newValues.append("0") 168 | # newValues = ", ".join(newValues) 169 | # newTransformType = "{" + newValues + "}" 170 | # # print(transformType) 171 | # # print(newValues) 172 | # # Copy the existing deltas to the 173 | # deltas = settings[deltaType][transformType] 174 | # newDeltas[newTransformType] = deltas 175 | # # settings[deltaType][newTransformType] = deltas 176 | # # del settings[deltaType][transformType] 177 | # # print(deltas) 178 | # settings[deltaType] = newDeltas 179 | # print(settings[deltaType]) 180 | 181 | print() 182 | print("DONE!") 183 | self.w.close() # delete if you want window to stay open 184 | except Exception as e: 185 | # brings macro window to front and reports error: 186 | Glyphs.showMacroWindow() 187 | print("Reorder Axes Error: %s" % e) 188 | import traceback 189 | print(traceback.format_exc()) 190 | 191 | 192 | ReorderAxes() 193 | -------------------------------------------------------------------------------- /Font/Replace in Family Name.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Replace in Family Name 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Finds and replaces in family name, including Variable Font Family Name and instances’ familyName custom parameters. Needs Vanilla. 8 | """ 9 | 10 | import vanilla 11 | from GlyphsApp import Glyphs 12 | 13 | 14 | class ReplaceInFamilyName(object): 15 | def __init__(self): 16 | # Window 'self.w': 17 | windowWidth = 250 18 | windowHeight = 145 19 | windowWidthResize = 100 # user can resize width by this value 20 | windowHeightResize = 0 # user can resize height by this value 21 | self.w = vanilla.Window( 22 | (windowWidth, windowHeight), # default window size 23 | "Replace in Family Name", # window title 24 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 25 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 26 | autosaveName="com.harbortype.ReplaceInFamilyName.mainwindow" # stores last window position and size 27 | ) 28 | 29 | # UI elements: 30 | linePos, inset, lineHeight = 22, 25, 25 31 | self.w.text_1 = vanilla.TextBox((inset - 1, linePos + 2, 75, 14), "Find:", sizeStyle='small') 32 | self.w.find = vanilla.EditText((inset + 80, linePos, -inset, 19), "", sizeStyle='small', callback=self.SavePreferences) 33 | linePos += lineHeight 34 | self.w.text_2 = vanilla.TextBox((inset - 1, linePos + 2, 75, 14), "Replace with:", sizeStyle='small') 35 | self.w.replace = vanilla.EditText((inset + 80, linePos, -inset, 19), "", sizeStyle='small', callback=self.SavePreferences) 36 | linePos += lineHeight 37 | 38 | # Run Button: 39 | self.w.runButton = vanilla.Button((-80 - inset, -20 - inset, -inset, -inset), "Run", sizeStyle='regular', callback=self.ReplaceInFamilyNameMain) 40 | self.w.setDefaultButton(self.w.runButton) 41 | 42 | # Load Settings: 43 | if not self.LoadPreferences(): 44 | print("Note: 'Replace in Family Name' could not load preferences. Will resort to defaults") 45 | 46 | # Open window and focus on it: 47 | self.w.open() 48 | self.w.makeKey() 49 | 50 | def SavePreferences(self, sender): 51 | try: 52 | Glyphs.defaults["com.harbortype.ReplaceInFamilyName.find"] = self.w.find.get() 53 | Glyphs.defaults["com.harbortype.ReplaceInFamilyName.replace"] = self.w.replace.get() 54 | except: 55 | return False 56 | return True 57 | 58 | def LoadPreferences(self): 59 | try: 60 | Glyphs.registerDefault("com.harbortype.ReplaceInFamilyName.find", "") 61 | Glyphs.registerDefault("com.harbortype.ReplaceInFamilyName.replace", "") 62 | self.w.find.set(Glyphs.defaults["com.harbortype.ReplaceInFamilyName.find"]) 63 | self.w.replace.set(Glyphs.defaults["com.harbortype.ReplaceInFamilyName.replace"]) 64 | except: 65 | return False 66 | return True 67 | 68 | def ReplaceInFamilyNameMain(self, sender): 69 | try: 70 | # update settings to the latest user input: 71 | if not self.SavePreferences(self): 72 | print("Note: 'Replace in Family Name' could not write preferences.") 73 | 74 | thisFont = Glyphs.font # frontmost font 75 | print("Replace in Family Name Report for %s" % thisFont.familyName) 76 | print(thisFont.filepath) 77 | print() 78 | 79 | findString = self.w.find.get() 80 | replaceString = self.w.replace.get() 81 | 82 | if not findString: 83 | raise Exception("The find string cannot be empty.") 84 | 85 | thisFont.disableUpdateInterface() 86 | 87 | # Replace in family name 88 | if findString in thisFont.familyName: 89 | oldFamilyName = thisFont.familyName 90 | newFamilyName = oldFamilyName.replace(findString, replaceString) 91 | thisFont.familyName = newFamilyName 92 | if oldFamilyName != newFamilyName: 93 | print("Font family name renamed from %s to %s" % (oldFamilyName, newFamilyName)) 94 | 95 | # Replace in variable font name 96 | if thisFont.customParameters["Variable Font Family Name"]: 97 | oldFamilyName = thisFont.customParameters["Variable Font Family Name"] 98 | newFamilyName = oldFamilyName.replace(findString, replaceString) 99 | thisFont.customParameters["Variable Font Family Name"] = newFamilyName 100 | if oldFamilyName != newFamilyName: 101 | print("Variable Font Family Name renamed from %s to %s" % (oldFamilyName, newFamilyName)) 102 | 103 | # Replace in custom parameters in instances 104 | for thisInstance in thisFont.instances: 105 | for parameter in thisInstance.customParameters: 106 | if parameter.name == "familyName": 107 | oldFamilyName = thisInstance.customParameters["familyName"] 108 | newFamilyName = oldFamilyName.replace(findString, replaceString) 109 | thisInstance.customParameters["familyName"] = newFamilyName 110 | if oldFamilyName != newFamilyName: 111 | print("Instance %s familyName renamed from %s to %s" % (thisInstance.name, oldFamilyName, newFamilyName)) 112 | elif parameter.name == "postscriptFontName": 113 | oldFamilyName = thisInstance.customParameters["postscriptFontName"] 114 | findStringPS = findString.replace(" ", "") 115 | replaceStringPS = replaceString.replace(" ", "") 116 | newFamilyName = oldFamilyName.replace(findStringPS, replaceStringPS) 117 | thisInstance.customParameters["postscriptFontName"] = newFamilyName 118 | if oldFamilyName != newFamilyName: 119 | print("Instance %s postscriptFontName renamed from %s to %s" % (thisInstance.name, oldFamilyName, newFamilyName)) 120 | 121 | self.w.close() # delete if you want window to stay open 122 | Glyphs.showMacroWindow() 123 | 124 | except Exception as e: 125 | # brings macro window to front and reports error: 126 | Glyphs.showMacroWindow() 127 | print("Replace in Family Name Error: %s" % e) 128 | import traceback 129 | print(traceback.format_exc()) 130 | 131 | finally: 132 | thisFont.enableUpdateInterface() 133 | 134 | 135 | ReplaceInFamilyName() 136 | -------------------------------------------------------------------------------- /Font/Report Windows Names.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Report Windows Names 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Checks for the length of nameID 1 and 4, which can cause issues in Word for Windows and Word for Mac respectively. 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | Glyphs.clearLog() # clears macro window log 13 | 14 | thisFont = Glyphs.font 15 | 16 | for thisInstance in thisFont.instances: 17 | nameLength = len(thisInstance.windowsFamily) 18 | flag = "🆗" 19 | delta = "" 20 | if nameLength > 31: 21 | flag = "⛔️" 22 | delta = "({})".format(31 - nameLength) 23 | elif nameLength == 31: 24 | flag = "⚠️" 25 | print("1 {} [{}] {} {}".format(flag, nameLength, thisInstance.windowsFamily, delta)) 26 | 27 | nameLength = len(thisInstance.fullName) 28 | flag = "🆗" 29 | delta = "" 30 | if nameLength > 31: 31 | flag = "⛔️" 32 | delta = "({})".format(31 - nameLength) 33 | elif nameLength == 31: 34 | flag = "⚠️" 35 | print("4 {} [{}] {} {}".format(flag, nameLength, thisInstance.fullName, delta)) 36 | 37 | nameLength = len(thisInstance.fontName) 38 | flag = "🆗" 39 | delta = "" 40 | if nameLength > 31: 41 | flag = "⛔️" 42 | delta = "({})".format(31 - nameLength) 43 | elif nameLength == 31: 44 | flag = "⚠️" 45 | print("6 {} [{}] {} {}".format(flag, nameLength, thisInstance.fontName, delta)) 46 | print("") 47 | 48 | Glyphs.showMacroWindow() 49 | -------------------------------------------------------------------------------- /Font/Sort Instances.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Sort Instances 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | import vanilla 6 | import collections 7 | from AppKit import NSDragOperationMove 8 | from GlyphsApp import Glyphs 9 | 10 | __doc__ = """ 11 | Sorts instances by axes values. Needs Vanilla. 12 | """ 13 | 14 | genericListPboardType = "genericListPboardType" 15 | 16 | 17 | class SortInstances(object): 18 | 19 | thisFont = Glyphs.font # frontmost font 20 | axes = collections.OrderedDict() 21 | for axis in thisFont.axes[:]: 22 | try: # Glyphs 3 23 | axes[axis.axisTag] = axis.name 24 | except: # Glyphs 2 25 | axes[axis["Tag"]] = axis["Name"] 26 | 27 | def __init__(self): 28 | # Window 'self.w': 29 | windowWidth = 280 30 | windowHeight = 190 31 | windowWidthResize = 200 # user can resize width by this value 32 | windowHeightResize = 200 # user can resize height by this value 33 | self.w = vanilla.Window( 34 | (windowWidth, windowHeight), # default window size 35 | "Sort Instances", # window title 36 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 37 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 38 | autosaveName="com.harbortype.SortInstances.mainwindow" # stores last window position and size 39 | ) 40 | 41 | # UI elements: 42 | linePos, inset, lineHeight = 12, 15, 22 43 | # self.w.text_1 = vanilla.TextBox((inset-1, linePos+2, 75, 14), "inset", sizeStyle='small') 44 | # linePos += lineHeight 45 | listValues = [] 46 | for i, (tag, name) in enumerate(self.axes.items()): 47 | listValues.append({"Index": i, "Name": name, "Tag": tag}) 48 | self.w.list_1 = vanilla.List( 49 | (inset, linePos, -inset, -inset - 20 - inset), 50 | listValues, 51 | columnDescriptions=[ 52 | {"title": "Index", "width": 40}, 53 | {"title": "Tag", "width": 60}, 54 | {"title": "Name"} 55 | ], 56 | allowsMultipleSelection=False, 57 | allowsEmptySelection=True, 58 | dragSettings=dict( 59 | type=genericListPboardType, 60 | operation=NSDragOperationMove, 61 | # allowDropBetweenRows=True, 62 | # allowDropOnRow=False, 63 | callback=self.dragCallback 64 | ), 65 | selfDropSettings=dict( 66 | type=genericListPboardType, 67 | operation=NSDragOperationMove, 68 | allowDropBetweenRows=True, 69 | # allowDropOnRow=False, 70 | callback=self.selfDropCallback 71 | ) 72 | ) 73 | linePos += lineHeight 74 | 75 | # Run Button: 76 | self.w.runButton = vanilla.Button((-80 - inset, -20 - inset, -inset, -inset), "Sort", sizeStyle='regular', callback=self.Process) 77 | self.w.setDefaultButton(self.w.runButton) 78 | 79 | # Open window and focus on it: 80 | self.w.open() 81 | self.w.makeKey() 82 | 83 | def dragCallback(self, sender, indexes): 84 | self.draggedItems = indexes 85 | 86 | def selfDropCallback(self, sender, dropInfo): 87 | isProposal = dropInfo["isProposal"] 88 | if not isProposal: 89 | target = dropInfo['rowIndex'] 90 | for original in self.draggedItems: 91 | newList = self.w.list_1.get() 92 | if target > original: 93 | newList.insert(target - 1, newList.pop(original)) 94 | else: 95 | newList.insert(target, newList.pop(original)) 96 | self.w.list_1.set(newList) 97 | return True 98 | 99 | def Process(self, sender): 100 | try: 101 | thisFont = Glyphs.font # frontmost font 102 | print("Sort Instances Report for %s" % thisFont.familyName) 103 | print(thisFont.filepath) 104 | print() 105 | 106 | newOrder = [item["Index"] for item in self.w.list_1.get()] 107 | print("Sorting instances by:") 108 | for i in newOrder: 109 | try: # Glyphs 3 110 | print(" ", thisFont.axes[i].name, i) 111 | except: # Glyphs 2 112 | print(" ", thisFont.axes[i]["Name"], i) 113 | print() 114 | 115 | # Sort the instances 116 | allInstances = thisFont.instances 117 | allInstances = sorted(allInstances, key=lambda inst: tuple(inst.axes[newOrder[i]] for i in range(len(newOrder)))) 118 | thisFont.instances = allInstances 119 | 120 | self.w.close() # delete if you want window to stay open 121 | except Exception as e: 122 | # brings macro window to front and reports error: 123 | Glyphs.showMacroWindow() 124 | print("Sort Instances Error: %s" % e) 125 | import traceback 126 | print(traceback.format_exc()) 127 | 128 | 129 | SortInstances() 130 | -------------------------------------------------------------------------------- /Glyph Names/Copy Sort Names.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Copy Sort Names from Background Font 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Copies the custom sortNames for all glyphs from the font in the background. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | Glyphs.clearLog() 11 | 12 | this_font = Glyphs.font 13 | 14 | if len(Glyphs.fonts) > 1: 15 | other_font = Glyphs.fonts[1] 16 | glyphs_to_process = [] 17 | 18 | for this_glyph in this_font.glyphs: 19 | if this_glyph.name not in other_font.glyphs: 20 | continue 21 | other_glyph = other_font.glyphs[this_glyph.name] 22 | this_sortName = this_glyph.sortName 23 | other_sortName = other_glyph.sortName 24 | if this_sortName != other_sortName: 25 | glyphs_to_process.append((this_glyph.name, other_sortName)) 26 | 27 | for glyphname, sortName in glyphs_to_process: 28 | this_glyph = this_font.glyphs[glyphname] 29 | this_glyph.sortName = sortName 30 | print(glyphname, this_glyph.sortName) 31 | 32 | else: 33 | Message( 34 | title="Copy Sort Names from Background Font", 35 | message="Only 1 file is open. Please open another Glyphs file.", 36 | ) 37 | -------------------------------------------------------------------------------- /Glyph Names/List Glyphs in Current Tab.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: List Glyphs in Current Tab 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Appends a line with the unique glyphs of the current tab. 8 | """ 9 | 10 | from GlyphsApp import Glyphs, Message 11 | 12 | thisFont = Glyphs.font 13 | currentTab = thisFont.currentTab 14 | 15 | if currentTab: 16 | 17 | allGlyphs = [x.name for x in thisFont.glyphs] 18 | uniqueGlyphs = [] 19 | 20 | for layer in currentTab.layers: 21 | glyph = layer.parent 22 | if glyph.name: 23 | if glyph.name in uniqueGlyphs: 24 | continue 25 | uniqueGlyphs.append(glyph.name) 26 | 27 | uniqueGlyphs.sort(key=lambda x: allGlyphs.index(x)) 28 | 29 | thisFont.currentText += "\n/" + "/".join(uniqueGlyphs) 30 | 31 | else: 32 | Message( 33 | title="List Glyphs in Current Tab", 34 | message="This script only works when an Edit tab is active.", 35 | OKButton="OK", 36 | ) 37 | -------------------------------------------------------------------------------- /Glyph Names/Rename Glyphs and Update Features.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Rename Glyphs and Update Features 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Renames glyphs and updates all classes and features. Will match either the entire glyph name or the dot suffix. 7 | """ 8 | 9 | import vanilla 10 | import copy 11 | from GlyphsApp import Glyphs, GSClass, GSFeature 12 | 13 | font = Glyphs.font 14 | 15 | 16 | class RenameGlyphs(object): 17 | 18 | def __init__(self): 19 | windowWidth = 280 20 | windowHeight = 115 21 | self.w = vanilla.FloatingWindow( 22 | (windowWidth, windowHeight), 23 | "Rename glyphs and update features", 24 | autosaveName="com.harbortype.RenameGlyphs.mainwindow" 25 | ) 26 | 27 | self.w.text_1 = vanilla.TextBox( 28 | (15, 18 + 2, 120, 17), 29 | "Find" 30 | ) 31 | self.w.findString = vanilla.EditText( 32 | (120, 18 - 1, -15, 22) 33 | ) 34 | 35 | self.w.text_2 = vanilla.TextBox( 36 | (15, 48 + 2, 120, 17), 37 | "Replace with" 38 | ) 39 | self.w.replaceString = vanilla.EditText( 40 | (120, 48 - 1, -15, 22) 41 | ) 42 | 43 | self.w.renameButton = vanilla.Button( 44 | (-130, -35, -15, -15), 45 | "Rename", 46 | callback=self.Main 47 | ) 48 | self.w.undoButton = vanilla.Button( 49 | (15, -35, 115, -15), 50 | "Undo", 51 | callback=self.Undo 52 | ) 53 | self.w.undoButton.enable(False) 54 | self.w.setDefaultButton(self.w.renameButton) 55 | 56 | # Load settings 57 | if not self.LoadPreferences(): 58 | print("Note: 'Rename Glyphs and Update Features' could not load preferences. Will resort to defaults.") 59 | 60 | self.w.open() 61 | self.w.makeKey() 62 | 63 | self.changedGlyphs = [] 64 | self.classesBackup = [] 65 | self.featuresBackup = [] 66 | 67 | def SavePreferences(self, sender): 68 | try: 69 | Glyphs.defaults["com.harbortype.RenameGlyphs.findString"] = self.w.findString.get() 70 | Glyphs.defaults["com.harbortype.RenameGlyphs.replaceString"] = self.w.replaceString.get() 71 | except: 72 | return False 73 | 74 | return True 75 | 76 | def LoadPreferences(self): 77 | try: 78 | Glyphs.registerDefault("com.harbortype.RenameGlyphs.findString", "ss01") 79 | Glyphs.registerDefault("com.harbortype.RenameGlyphs.replaceString", "ss02") 80 | self.w.findString.set(Glyphs.defaults["com.harbortype.RenameGlyphs.findString"]) 81 | self.w.replaceString.set(Glyphs.defaults["com.harbortype.RenameGlyphs.replaceString"]) 82 | except: 83 | return False 84 | 85 | return True 86 | 87 | def Undo(self, sender): 88 | font.disableUpdateInterface() 89 | oldNames = [glyphNames["old"] for glyphNames in self.changedGlyphs] 90 | newNames = [glyphNames["new"] for glyphNames in self.changedGlyphs] 91 | for i in range(len(newNames)): 92 | font.glyphs[newNames[i]].name = oldNames[i] 93 | # Restore the classes and features backup 94 | font.classes = self.classesBackup 95 | font.features = self.featuresBackup 96 | # Empty the changed glyphs list 97 | self.changedGlyphs = [] 98 | self.w.undoButton.enable(False) 99 | font.enableUpdateInterface() 100 | 101 | def ReplaceInCode(self, oldNames, newNames, code): 102 | for i in range(len(oldNames)): 103 | code = code.replace(oldNames[i], newNames[i]) 104 | return code 105 | 106 | def RenameFeatures(self, oldNames, newNames): 107 | # Stitch together classes and features in a single loop 108 | for classes_and_features in [font.classes, font.features]: 109 | # Loop through classes and features in reverse order 110 | # because it might delete some automatic features 111 | for f in range(len(classes_and_features) - 1, -1, -1): 112 | class_or_feature = classes_and_features[f] 113 | if isinstance(class_or_feature, GSClass): 114 | objType = "class" 115 | else: 116 | objType = "feature" 117 | # Catch an empty feature when first run 118 | if not class_or_feature: 119 | continue 120 | # Trigger update if automatic 121 | if class_or_feature.automatic: 122 | # Only update if glyphname appears in the class/feature 123 | if any(glyphName in class_or_feature.code for glyphName in oldNames): 124 | class_or_feature.update() 125 | print("Updated %s %s" % (objType, class_or_feature.name)) 126 | # Find and replace in manual features 127 | else: 128 | code = self.ReplaceInCode(oldNames, newNames, class_or_feature.code) 129 | if isinstance(class_or_feature, GSClass): 130 | font.classes[class_or_feature.name].code = code 131 | elif isinstance(class_or_feature, GSFeature): 132 | font.features[class_or_feature.name].code = code 133 | print("Replaced in %s %s" % (objType, class_or_feature.name)) 134 | 135 | def Main(self, sender): 136 | 137 | Glyphs.clearLog() 138 | self.SavePreferences(sender) 139 | font.disableUpdateInterface() 140 | 141 | print("Rename Glyphs and Update Features for %s" % font.familyName) 142 | print(font.filepath) 143 | print() 144 | 145 | findString = self.w.findString.get() 146 | replaceString = self.w.replaceString.get() 147 | namesDict = {} 148 | 149 | # Store classes and features for undo 150 | self.classesBackup = copy.copy(font.classes) 151 | self.featuresBackup = copy.copy(font.features) 152 | self.w.undoButton.enable(True) 153 | 154 | for glyph in font.glyphs: 155 | 156 | # Exact match 157 | if findString == glyph.name: 158 | glyph.beginUndo() 159 | originalName = glyph.name 160 | glyph.name = replaceString 161 | newName = glyph.name 162 | namesDict[originalName] = newName 163 | glyph.endUndo() 164 | self.changedGlyphs.append({ 165 | "old": originalName, 166 | "new": newName 167 | }) 168 | print(originalName, ">", newName) 169 | 170 | # Substring match 171 | elif findString in glyph.name.split("."): 172 | glyph.beginUndo() 173 | originalName = glyph.name 174 | glyph.name = glyph.name.replace(findString, replaceString) 175 | newName = glyph.name 176 | namesDict[originalName] = newName 177 | glyph.endUndo() 178 | self.changedGlyphs.append({ 179 | "old": originalName, 180 | "new": newName 181 | }) 182 | print(originalName, ">", newName) 183 | 184 | oldNames = [glyphNames["old"] for glyphNames in self.changedGlyphs] 185 | newNames = [glyphNames["new"] for glyphNames in self.changedGlyphs] 186 | self.RenameFeatures(oldNames, newNames) 187 | 188 | font.enableUpdateInterface() 189 | 190 | 191 | RenameGlyphs() 192 | -------------------------------------------------------------------------------- /Hinting/Export Hinting HTML Test Page.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Export Hinting Test Page (HTML) 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Create a Test HTML for the current font inside the current Webfont Export folder, or for the current Glyphs Project in the project’s export path. 8 | Based on mekkablue's Webfont Test HTML script. 9 | """ 10 | 11 | from os import system 12 | from GlyphsApp import Glyphs, GSProjectDocument, Message 13 | 14 | fileFormats = ("woff") 15 | 16 | 17 | def saveFileInLocation(content="blabla", fileName="test.txt", filePath="~/Desktop"): 18 | saveFileLocation = "%s/%s" % (filePath, fileName) 19 | saveFileLocation = saveFileLocation.replace("//", "/") 20 | f = open(saveFileLocation, 'w') 21 | print("Exporting to:", f.name) 22 | f.write(content) 23 | f.close() 24 | return True 25 | 26 | 27 | def currentWebExportPath(): 28 | exportPath = Glyphs.defaults["WebfontPluginExportPathManual"] 29 | if Glyphs.defaults["WebfontPluginUseExportPath"]: 30 | exportPath = Glyphs.defaults["WebfontPluginExportPath"] 31 | return exportPath 32 | 33 | 34 | def replaceSet(text, setOfReplacements): 35 | for thisReplacement in setOfReplacements: 36 | searchFor = thisReplacement[0] 37 | replaceWith = thisReplacement[1] 38 | text = text.replace(searchFor, replaceWith) 39 | return text 40 | 41 | 42 | def allUnicodeEscapesOfFont(thisFont): 43 | allUnicodes = ["%s;" % g.unicode for g in thisFont.glyphs if g.unicode and g.export] 44 | return " ".join(allUnicodes) 45 | 46 | 47 | def getInstanceInfo(thisFont, activeInstance, fileFormat): 48 | # Determine Family Name 49 | familyName = thisFont.familyName 50 | individualFamilyName = activeInstance.customParameters["familyName"] 51 | if individualFamilyName is not None: 52 | familyName = individualFamilyName 53 | 54 | # Determine Style Name 55 | activeInstanceName = activeInstance.name 56 | 57 | # Determine font and file names for CSS 58 | menuName = "%s %s-%s" % (fileFormat.upper(), familyName, activeInstanceName) 59 | 60 | firstPartOfFileName = activeInstance.customParameters["fileName"] 61 | if not firstPartOfFileName: 62 | firstPartOfFileName = "%s-%s" % (familyName.replace(" ", ""), activeInstanceName.replace(" ", "")) 63 | 64 | fileName = "%s.%s" % (firstPartOfFileName, fileFormat) 65 | return fileName, menuName, activeInstanceName 66 | 67 | 68 | def activeInstancesOfFont(thisFont, fileFormats=fileFormats): 69 | activeInstances = [i for i in thisFont.instances if i.active] 70 | listOfInstanceInfo = [] 71 | for fileFormat in fileFormats: 72 | for activeInstance in activeInstances: 73 | fileName, menuName, activeInstanceName = getInstanceInfo(thisFont, activeInstance, fileFormat) 74 | listOfInstanceInfo.append((fileName, menuName, activeInstanceName)) 75 | return listOfInstanceInfo 76 | 77 | 78 | def activeInstancesOfProject(thisProject, fileFormats=fileFormats): 79 | thisFont = thisProject.font() 80 | activeInstances = [i for i in thisProject.instances() if i.active] 81 | listOfInstanceInfo = [] 82 | for fileFormat in fileFormats: 83 | for activeInstance in activeInstances: 84 | fileName, menuName, activeInstanceName = getInstanceInfo(thisFont, activeInstance, fileFormat) 85 | listOfInstanceInfo.append((fileName, menuName, activeInstanceName)) 86 | return listOfInstanceInfo 87 | 88 | 89 | def optionListForInstances(instanceList): 90 | returnString = "" 91 | for thisInstanceInfo in instanceList: 92 | returnString += ' \n' % (thisInstanceInfo[0], thisInstanceInfo[1]) 93 | # 94 | 95 | return returnString 96 | 97 | 98 | def fontFaces(instanceList): 99 | returnString = "" 100 | for thisInstanceInfo in instanceList: 101 | fileName = thisInstanceInfo[0] 102 | nameOfTheFont = thisInstanceInfo[1] 103 | returnString += "\t\t@font-face { font-family: '%s'; src: url('%s'); }\n" % (nameOfTheFont, fileName) 104 | 105 | return returnString 106 | 107 | 108 | def featureListForFont(thisFont): 109 | returnString = "" 110 | featureList = [f.name for f in thisFont.features if f.name not in ("ccmp", "aalt", "locl", "kern", "calt", "liga", "clig") and not f.disabled()] 111 | for f in featureList: 112 | returnString += """ """ % (f, f, f) 113 | return returnString 114 | 115 | 116 | htmlContent = """
117 | 118 | 119 |228 | Charset 229 | Lat1 230 | 231 | eot 232 | woff 233 | woff2 234 | 235 | OT Features: 236 | 237 | 238 | 239 | 240 | 241 |
242 | 243 |08 ABCDEFGHIJKLMNOPQRSTUVWXYZ
244 |09 ABCDEFGHIJKLMNOPQRSTUVWXYZ
245 |10 ABCDEFGHIJKLMNOPQRSTUVWXYZ
246 |11 ABCDEFGHIJKLMNOPQRSTUVWXYZ
247 |12 ABCDEFGHIJKLMNOPQRSTUVWXYZ
248 |13 ABCDEFGHIJKLMNOPQRSTUVWXYZ
249 |14 ABCDEFGHIJKLMNOPQRSTUVWXYZ
250 |15 ABCDEFGHIJKLMNOPQRSTUVWXYZ
251 |16 ABCDEFGHIJKLMNOPQRSTUVWXYZ
252 | 253 | """ 254 | 255 | # brings macro window to front and clears its log: 256 | Glyphs.clearLog() 257 | Glyphs.showMacroWindow() 258 | 259 | # Query app version: 260 | GLYPHSAPPVERSION = Glyphs.versionNumber 261 | appVersionHighEnough = not GLYPHSAPPVERSION.startswith("1.") 262 | 263 | if appVersionHighEnough: 264 | firstDoc = Glyphs.orderedDocuments()[0] 265 | if firstDoc.isKindOfClass_(GSProjectDocument): 266 | thisFont = firstDoc.font() # frontmost project file 267 | firstActiveInstance = [i for i in firstDoc.instances() if i.active][0] 268 | activeFontInstances = activeInstancesOfProject(firstDoc) 269 | exportPath = firstDoc.exportPath() 270 | else: 271 | thisFont = Glyphs.font # frontmost font 272 | firstActiveInstance = [i for i in thisFont.instances if i.active][0] 273 | activeFontInstances = activeInstancesOfFont(thisFont) 274 | exportPath = currentWebExportPath() 275 | 276 | familyName = thisFont.familyName 277 | 278 | print("Preparing Test HTML for:") 279 | for thisFontInstanceInfo in activeFontInstances: 280 | print(" %s" % thisFontInstanceInfo[1]) 281 | 282 | optionList = optionListForInstances(activeFontInstances) 283 | fontFacesCSS = fontFaces(activeFontInstances) 284 | firstFileName = activeFontInstances[0][0] 285 | firstFontName = activeFontInstances[0][1] 286 | 287 | replacements = ( 288 | ("familyName", familyName), 289 | ("nameOfTheFont", firstFontName), 290 | ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", allUnicodeEscapesOfFont(thisFont)), 291 | ("fileName", firstFileName), 292 | (" \n", optionList), 293 | (" \n", featureListForFont(thisFont)), 294 | (" \n", fontFacesCSS) 295 | ) 296 | 297 | htmlContent = replaceSet(htmlContent, replacements) 298 | 299 | # Write file to disk: 300 | if exportPath: 301 | if saveFileInLocation(content=htmlContent, fileName="fonttest.html", filePath=exportPath): 302 | print("Successfully wrote file to disk.") 303 | terminalCommand = 'cd "%s"; open .' % exportPath 304 | system(terminalCommand) 305 | else: 306 | print("Error writing file to disk.") 307 | else: 308 | Message( 309 | title="Webfont Test HTML Error", 310 | message="Could not determine export path. Have you exported any webfonts yet?", 311 | OKButton=None 312 | ) 313 | else: 314 | print("This script requires Glyphs 2. Sorry.") 315 | -------------------------------------------------------------------------------- /Hinting/New Tab with Rotated, Scaled or Flipped Components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Rotated, Scaled or Flipped Components 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new edit tab with components that were rotated, scaled or flipped. They may cause issues on TrueType. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | thisFontMaster = thisFont.selectedFontMaster # active master 12 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 13 | 14 | 15 | def containsTransformedComponents(thisGlyph): 16 | for thisLayer in thisGlyph.layers: 17 | if thisLayer.layerId != thisLayer.associatedMasterId and not thisLayer.isSpecialLayer: 18 | continue 19 | for thisComponent in thisLayer.components: 20 | if thisComponent.transform[:4] != (1.0, 0.0, 0.0, 1.0): 21 | print(thisGlyph.name, thisComponent) 22 | return True 23 | if thisComponent.rotation != 0.0: 24 | print(thisGlyph.name, thisComponent) 25 | return True 26 | return False 27 | 28 | 29 | glyphList = [] 30 | 31 | for thisGlyph in thisFont.glyphs: 32 | if containsTransformedComponents(thisGlyph): 33 | glyphList.append(thisGlyph.name) 34 | 35 | if glyphList: 36 | tabString = "/" + "/".join(glyphList) 37 | thisFont.newTab(tabString) 38 | else: 39 | Message( 40 | title="No Transformed Components", 41 | message="No rotated, mirrored, or flipped components found in this font.", 42 | OKButton="Yeah" 43 | ) 44 | -------------------------------------------------------------------------------- /Hinting/New Tab with Vertically Shifted Components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Vertically Shifted Components 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Opens a new edit tab with components that are transformed beyond mere horizontal shifts. 8 | """ 9 | 10 | from GlyphsApp import Glyphs, Message 11 | 12 | thisFont = Glyphs.font # frontmost font 13 | thisFontMaster = thisFont.selectedFontMaster # active master 14 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 15 | 16 | 17 | def containsTransformedComponents(thisGlyph): 18 | for thisLayer in thisGlyph.layers: 19 | if thisLayer.isMasterLayer or thisLayer.isSpecialLayer: 20 | for thisComponent in thisLayer.components: 21 | if thisComponent.transform[-1] != 0.0 and thisComponent.component.export: 22 | print("%s: component %s is shifted by %s on layer %s." % (thisGlyph.name, thisComponent.name, thisComponent.transform[-1], thisLayer.name)) 23 | return True 24 | return False 25 | 26 | 27 | glyphList = [] 28 | 29 | for thisGlyph in thisFont.glyphs: 30 | if containsTransformedComponents(thisGlyph): 31 | glyphList.append(thisGlyph.name) 32 | 33 | if glyphList: 34 | tabString = "/" + "/".join(glyphList) 35 | thisFont.newTab(tabString) 36 | else: 37 | Message( 38 | title="No Transformed Components", 39 | message="No vertically shifted components found in this font.", 40 | OKButton="Yeah" 41 | ) 42 | -------------------------------------------------------------------------------- /Hinting/Reverse PS Hint.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Reverse PS Hint 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Reverses the direction of the selected PS hints. 6 | """ 7 | 8 | this_font = Glyphs.font 9 | 10 | for this_layer in this_font.selectedLayers: 11 | try: 12 | this_font.disableUpdateInterface() 13 | for this_hint in this_layer.hints: 14 | if this_hint.selected and this_hint.isPostScript: 15 | if this_hint.type != STEM: 16 | continue 17 | origin_node = this_hint.originNode 18 | target_node = this_hint.targetNode 19 | this_hint.originNode = target_node 20 | this_hint.targetNode = origin_node 21 | except Exception as e: 22 | raise e 23 | finally: 24 | this_font.enableUpdateInterface() 25 | -------------------------------------------------------------------------------- /Interpolation/New Tab with Repeating Components and Paths.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Repeating Components and Paths 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Opens a new Edit tab with glyphs that contain multiple instances of the same component or path. They might be interpolating with the wrong ones! 8 | """ 9 | 10 | from GlyphsApp import Glyphs, Message 11 | 12 | thisFont = Glyphs.font 13 | txt = "" 14 | 15 | for thisGlyph in thisFont.glyphs: 16 | firstLayer = thisGlyph.layers[0] 17 | # Check for repeating components 18 | thisGlyphComponents = [] 19 | for thisComponent in firstLayer.components: 20 | if thisComponent.name in thisGlyphComponents: 21 | txt += "/{0}".format(thisGlyph.name) 22 | break 23 | thisGlyphComponents.append(thisComponent.name) 24 | # Check for paths with the same number of nodes in the glyph 25 | thisGlyphPaths = [] 26 | for thisPath in firstLayer.paths: 27 | nodeOrder = tuple([node.type for node in thisPath.nodes]) 28 | if nodeOrder in thisGlyphPaths: 29 | txt += "/{0}".format(thisGlyph.name) 30 | break 31 | thisGlyphPaths.append(nodeOrder) 32 | 33 | if txt: 34 | Glyphs.font.newTab(txt) 35 | else: 36 | Message( 37 | title="New Tab with Repeating Components", 38 | message="No glyphs with repeating components in this font.", 39 | OKButton="OK", 40 | ) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /Layers/Copy Master into Sublayer.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Copy Master into Sublayer... 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Copies a master into a sublayer of another master for the selected glyphs. Useful for creating COLR/CPAL color fonts. Based on @mekkablue's Copy Layer to Layer script. 8 | """ 9 | 10 | import vanilla 11 | from GlyphsApp import Glyphs, GSLayer 12 | 13 | thisFont = Glyphs.font 14 | currentMaster = thisFont.selectedFontMaster 15 | 16 | # TODO Glyphs 3 compatibility 17 | 18 | 19 | class CopyMasterIntoSublayer(object): 20 | 21 | def __init__(self): 22 | windowWidth = 280 23 | windowHeight = 150 24 | windowWidthResize = 0 25 | windowHeightResize = 0 26 | self.w = vanilla.FloatingWindow( 27 | (windowWidth, windowHeight), 28 | "Copy master into sublayer", 29 | minSize=(windowWidth, windowHeight), 30 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), 31 | autosaveName="com.harbortype.CopyMasterIntoSublayer.mainwindow" 32 | ) 33 | 34 | self.w.text_1 = vanilla.TextBox((15, 12 + 2, 120, 14), "Copy paths from:", sizeStyle='small') 35 | self.w.masterSource = vanilla.PopUpButton((120, 12, -15, 17), self.GetMasterNames(), sizeStyle='small', callback=None) 36 | 37 | self.w.text_2 = vanilla.TextBox((15, 48 + 2, 120, 14), "into the sublayer:", sizeStyle='small') 38 | self.w.layerTarget = vanilla.EditText((120, 48 - 1, -15, 18), sizeStyle='small', callback=None) 39 | 40 | self.w.text_3 = vanilla.TextBox((15, 48 + 22, 120, 14), "of master:", sizeStyle='small') 41 | self.w.masterDestination = vanilla.PopUpButton((120, 48 + 20, -15, 17), self.GetMasterNames(), sizeStyle='small', callback=None) 42 | 43 | self.w.copybutton = vanilla.Button((-80, -30, -15, -10), "Copy", sizeStyle='small', callback=self.CopyAll) 44 | self.w.setDefaultButton(self.w.copybutton) 45 | 46 | # Load Settings: 47 | if not self.LoadPreferences(): 48 | print("Note: 'Copy Master into Sublayer' could not load preferences. Will resort to defaults.") 49 | 50 | self.w.open() 51 | self.w.makeKey() 52 | 53 | def SavePreferences(self, sender): 54 | try: 55 | Glyphs.defaults["com.harbortype.CopyMasterIntoSublayer.layerTarget"] = self.w.layerTarget.get() 56 | except: 57 | return False 58 | 59 | return True 60 | 61 | def LoadPreferences(self): 62 | try: 63 | Glyphs.registerDefault("com.harbortype.CopyMasterIntoSublayer.layerTarget", "Color 0") 64 | self.w.layerTarget.set(Glyphs.defaults["com.harbortype.CopyMasterIntoSublayer.layerTarget"]) 65 | except: 66 | return False 67 | 68 | return True 69 | 70 | def GetMasterNames(self): 71 | """Collects names of masters to populate the submenu in the GUI.""" 72 | myMasterList = [] 73 | for masterIndex in range(len(thisFont.masters)): 74 | thisMaster = thisFont.masters[masterIndex] 75 | myMasterList.append('%i: %s' % (masterIndex, thisMaster.name)) 76 | return myMasterList 77 | 78 | def CheckIfSublayerExists(self, masterId, sublayerName, glyph): 79 | """Checks if there is a sublayer of the same name. 80 | If it does, clear the layer. 81 | If it doesn't, create the layer.""" 82 | for layer in glyph.layers: 83 | # Find a layer with the name informed by the user and associated with the current master. 84 | # Effectively, this will loop through all layers in all masters and return only if the layer 85 | # is a child of the current master (associatedMasterId) 86 | if layer.name == sublayerName and layer.associatedMasterId == masterId: 87 | return layer 88 | return None 89 | 90 | def CreateEmptySublayer(self, masterId, sublayerName, glyph): 91 | """Creates a new empty GSLayer object and appends it to the current glyph under the current master.""" 92 | newLayer = GSLayer() 93 | newLayer.name = sublayerName 94 | newLayer.associatedMasterId = masterId 95 | glyph.layers.append(newLayer) 96 | 97 | def ClearSublayer(self, sublayer): 98 | """Clears all content from the current layer.""" 99 | try: 100 | # GLYPHS 3 101 | sublayer.shapes = None 102 | except: 103 | # GLYPHS 2 104 | sublayer.paths = None 105 | sublayer.components = None 106 | sublayer.anchors = None 107 | sublayer.background = None 108 | 109 | def CopyAll(self, sender): 110 | """Copies all data into the sublayer.""" 111 | sublayerName = self.w.layerTarget.get() 112 | indexOfMasterSource = self.w.masterSource.get() 113 | indexOfMasterDestination = self.w.masterDestination.get() 114 | masterDestinationId = thisFont.masters[indexOfMasterDestination].id 115 | # Gets the selected glyphs 116 | selectedGlyphs = [x.parent for x in thisFont.selectedLayers] 117 | 118 | # For each selected glyph 119 | for glyph in selectedGlyphs: 120 | 121 | # Prepare sublayer 122 | sublayer = self.CheckIfSublayerExists(masterDestinationId, sublayerName, glyph) 123 | if sublayer: 124 | self.ClearSublayer(sublayer) 125 | else: 126 | self.CreateEmptySublayer(masterDestinationId, sublayerName, glyph) 127 | sublayer = self.CheckIfSublayerExists(masterDestinationId, sublayerName, glyph) 128 | 129 | # Copy paths, components and anchors 130 | try: 131 | # GLYPHS 3 132 | sublayer.shapes = glyph.layers[indexOfMasterSource].shapes 133 | except: 134 | # GLYPHS 2 135 | sublayer.paths = glyph.layers[indexOfMasterSource].paths 136 | sublayer.components = glyph.layers[indexOfMasterSource].components 137 | sublayer.anchors = glyph.layers[indexOfMasterSource].anchors 138 | sublayer.width = glyph.layers[indexOfMasterSource].width 139 | 140 | 141 | CopyMasterIntoSublayer() 142 | -------------------------------------------------------------------------------- /Layers/Remove all layers for the current master.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove all layers for the current master 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Deletes all non-master layers for the current master (including bracket and brace layers) in selected glyphs. 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | # Glyphs.showMacroWindow() 13 | # Glyphs.clearLog() 14 | 15 | font = Glyphs.font 16 | selectedLayers = font.selectedLayers 17 | 18 | font.disableUpdateInterface() 19 | 20 | for thisLayer in selectedLayers: 21 | thisGlyph = thisLayer.parent 22 | 23 | thisGlyph.beginUndo() 24 | 25 | # Build list of layers to be deleted 26 | layersToBeDeleted = [] 27 | for currentLayer in thisGlyph.layers: 28 | if currentLayer.associatedMasterId == thisLayer.layerId: 29 | if currentLayer.associatedMasterId != currentLayer.layerId: 30 | layersToBeDeleted.append(currentLayer.layerId) 31 | 32 | # Delete layers 33 | if len(layersToBeDeleted) > 0: 34 | for id in layersToBeDeleted: 35 | del thisGlyph.layers[id] 36 | 37 | thisGlyph.endUndo() 38 | 39 | font.enableUpdateInterface() 40 | -------------------------------------------------------------------------------- /Metrics and Kerning/Copy Kerning Groups from Unsuffixed Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Copy Kerning Groups from Unsuffixed Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Copies the kerning groups from the default (unsuffixed) glyphs to the selected ones. The selected glyphs need to have a suffix, otherwise they will be skipped. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Glyphs.clearLog() 11 | 12 | font = Glyphs.font 13 | 14 | for selected_layer in font.selectedLayers: 15 | this_glyph = selected_layer.parent 16 | glyph_name = this_glyph.name 17 | 18 | if "." not in glyph_name: 19 | print("No changes made to {} because if doesn't have a suffix".format( 20 | glyph_name 21 | )) 22 | continue 23 | 24 | default_glyph_name = glyph_name[:glyph_name.index(".")] 25 | if default_glyph_name not in font.glyphs: 26 | print("{} does not exist in this font.".format( 27 | default_glyph_name 28 | )) 29 | continue 30 | default_glyph = font.glyphs[default_glyph_name] 31 | 32 | this_glyph.leftKerningGroup = default_glyph.leftKerningGroup 33 | this_glyph.rightKerningGroup = default_glyph.rightKerningGroup 34 | print("Groups copied from {} to {}".format( 35 | default_glyph_name, 36 | glyph_name 37 | )) 38 | 39 | Glyphs.showMacroWindow() 40 | -------------------------------------------------------------------------------- /Metrics and Kerning/New Tab with Kerning Exceptions.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Kerning Exceptions 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new Edit tab containing all kerning exceptions for the current master. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | tabText = "" 11 | font = Glyphs.font 12 | currentMasterID = font.selectedFontMaster.id 13 | masterKerning = font.kerning[currentMasterID] 14 | 15 | 16 | def GetGlyphName(kerning_key): 17 | if kerning_key[0] == "@": 18 | return kerning_key[7:] 19 | else: 20 | return font.glyphForId_(kerning_key).name 21 | 22 | 23 | for leftSide in masterKerning.keys(): 24 | left_glyph = GetGlyphName(leftSide) 25 | 26 | if leftSide[0] != "@" and font.glyphs[left_glyph].rightKerningGroup: 27 | for rightSide in masterKerning[leftSide].keys(): 28 | right_glyph = GetGlyphName(rightSide) 29 | tabText += "nn/%s/%s nn\n" % (left_glyph, right_glyph) 30 | else: 31 | for rightSide in masterKerning[leftSide].keys(): 32 | right_glyph = GetGlyphName(rightSide) 33 | if rightSide[0] != "@" and font.glyphs[right_glyph].leftKerningGroup: 34 | tabText += "nn/%s/%s nn\n" % (left_glyph, right_glyph) 35 | 36 | font.newTab(tabText.strip()) 37 | -------------------------------------------------------------------------------- /Metrics and Kerning/New Tab with Kerning Pairs for Selected Glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Kerning Pairs for Selected Glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Opens a new tab with kerning pairs for the selected glyph (minus diacritics) 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | # Glyphs.clearLog() 12 | 13 | 14 | def nameForID(font, ID): 15 | if ID[0] == "@": # is a group 16 | return ID 17 | else: # is a glyph 18 | return font.glyphForId_(ID).name 19 | 20 | 21 | font = Glyphs.font 22 | masterID = font.selectedFontMaster.id 23 | masterKernDict = font.kerning[masterID] 24 | text = '' 25 | exclude = ["acute", "breve", "caron", "cedilla", "circumflex", "commaaccent", "dieresis", "dotaccent", "grave", "hungarumlaut", "macron", "ogonek", "ring", "tilde"] 26 | 27 | glyph = font.selectedLayers[0].parent 28 | glyphName = glyph.name 29 | glyphID = glyph.id 30 | 31 | leftKey = glyph.leftKerningKey 32 | rightKey = glyph.rightKerningKey 33 | 34 | if leftKey[0] == '@': 35 | leftClass = leftKey 36 | else: 37 | leftClass = font.glyphs[glyphName].id 38 | 39 | if rightKey[0] == '@': 40 | rightClass = rightKey 41 | else: 42 | rightClass = font.glyphs[glyphName].id 43 | 44 | rightGlyphs = [] 45 | if rightClass in masterKernDict.keys(): 46 | for rightPair in masterKernDict[rightClass].keys(): 47 | if rightPair[0] == '@': 48 | for g in font.glyphs: 49 | if g.leftKerningKey == rightPair: 50 | rightGlyphs.append(g.name) 51 | else: 52 | rightGlyphs.append(nameForID(font, rightPair)) 53 | 54 | filterDiacritics = lambda s: not any(x in s for x in exclude) 55 | rightGlyphs = list(filter(filterDiacritics, rightGlyphs)) 56 | 57 | for r in rightGlyphs: 58 | text += "/%s/%s/space" % (glyphName, r) 59 | text += "\n" 60 | 61 | leftGlyphs = [] 62 | for leftPair, rightKernDict in masterKernDict.items(): 63 | if leftClass in rightKernDict.keys(): 64 | if leftPair[0] == '@': 65 | for g in font.glyphs: 66 | if g.rightKerningKey == leftPair: 67 | leftGlyphs.append(g.name) 68 | else: 69 | leftGlyphs.append(nameForID(font, leftPair)) 70 | 71 | filterDiacritics = lambda s: not any(x in s for x in exclude) 72 | leftGlyphs = list(filter(filterDiacritics, leftGlyphs)) 73 | 74 | for L in leftGlyphs: 75 | text += '/%s/%s/space' % (L, glyphName) 76 | 77 | # print(text) 78 | 79 | if text: 80 | font.newTab(text) 81 | -------------------------------------------------------------------------------- /Metrics and Kerning/New Tab with Missing Kerning Pairs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Missing Kerning Pairs 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Compares two glyphs files and opens a new tab with missing kerning pairs for the current master 8 | """ 9 | 10 | from GlyphsApp import Glyphs 11 | 12 | Glyphs.clearLog() 13 | 14 | 15 | # generate glyph names to display in the tabs 16 | def glyphStr(f, g): 17 | if "@" not in g: 18 | # if it's not a kerning group, it's a glyph id. 19 | # iterate through the glyphs in the font to find the name. 20 | return "/" + f.glyphForId_(g).name 21 | elif "@MMK" in g: 22 | # if it's a group, just get the last characters in the name 23 | return "/" + str(g)[7:] 24 | 25 | 26 | def getID(g1, g2): 27 | print(g1, g2) 28 | # print(type(font1.glyphs[g1]), type(font1.glyphs[g2])) 29 | 30 | # if font1.glyphs[g1] is not None: 31 | if font1.glyphs[g1].rightKerningKey: 32 | g1 = font1.glyphs[g1].rightKerningKey 33 | # if font1.glyphs[g2] is not None: 34 | if font1.glyphs[g2].leftKerningKey: 35 | g2 = font1.glyphs[g2].leftKerningKey 36 | # g1 = font1.glyphs[g1].rightKerningKey 37 | # g2 = font1.glyphs[g2].leftKerningKey 38 | # if "@" not in g1: 39 | # g1 = font1.glyphs[g1].id 40 | # if "@" not in g2: 41 | # g2 = font1.glyphs[g2].id 42 | return g1, g2 43 | 44 | 45 | font1 = Glyphs.fonts[0] 46 | font2 = Glyphs.fonts[1] 47 | 48 | masterID = font1.selectedFontMaster.id 49 | print(masterID) 50 | 51 | kerningFront = font1.kerning[masterID] 52 | kerningBack = font2.kerning[masterID] 53 | 54 | text = [] 55 | 56 | pairsFront = [] 57 | pairsBack = [] 58 | 59 | print(kerningFront) 60 | 61 | for leftG in kerningFront: 62 | for rightG in kerningFront[leftG]: 63 | currentPair = [glyphStr(font1, leftG), glyphStr(font1, rightG)] 64 | currentPair = ''.join(currentPair) 65 | pairsFront.append(currentPair) 66 | 67 | for leftG in kerningBack: 68 | for rightG in kerningBack[leftG]: 69 | currentPair = [glyphStr(font2, leftG), glyphStr(font2, rightG)] 70 | currentPair = ''.join(currentPair) 71 | pairsBack.append(currentPair) 72 | 73 | for pair in pairsBack: 74 | if pair not in pairsFront: 75 | glyphs = pair[1:].split("/") 76 | leftG, rightG = getID(glyphs[0], glyphs[1]) 77 | print(leftG, rightG) 78 | kernValue = font1.kerningForPair(masterID, leftG, rightG) 79 | if kernValue > 100000: 80 | text.append("/n/n" + pair + "/n/n\n") 81 | 82 | text = ''.join(text) 83 | font1.newTab(text) 84 | -------------------------------------------------------------------------------- /Metrics and Kerning/New Tab with Zero Kerning Pairs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Zero Kerning Pairs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Opens a new tab with missing kerning pairs (value set as zero) for each master 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | font = Glyphs.font 12 | 13 | 14 | # generate glyph names to display in the tabs 15 | def list_name(kerning_key): 16 | if "@MMK" in kerning_key: 17 | # if it's a group, just get the last characters in the name 18 | return "/" + str(kerning_key)[7:] 19 | if "@" not in kerning_key: 20 | # if it's not a kerning group, it's a glyph id. 21 | # iterate through the glyphs in the font to find the name. 22 | return "/" + str(font.glyphForId_(kerning_key).name) 23 | # if everything fails, returns an empty string 24 | return "" 25 | 26 | 27 | # iterate through the kerning dictionary of each master 28 | # to find pairs with value set as zero 29 | for master in font.masters: 30 | zeroList = [str(master.name), "\n"] 31 | for firstG in font.kerning[master.id]: 32 | for secondG in font.kerning[master.id][firstG]: 33 | value = font.kerning[master.id][firstG][secondG] 34 | if value == 0: 35 | zeroList.append(str(list_name(firstG))) 36 | zeroList.append(str(list_name(secondG))) 37 | zeroList.append("\n") 38 | font.newTab("".join(zeroList)) 39 | -------------------------------------------------------------------------------- /Metrics and Kerning/Round All Kerning.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Round All Kerning 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Rounds all kerning pairs of the current master (or all masters) according a multiple provided. Requires vanilla. 6 | """ 7 | 8 | import vanilla 9 | from Foundation import NSNumberFormatter 10 | from GlyphsApp import Glyphs 11 | 12 | font = Glyphs.font 13 | currentMaster = font.selectedFontMaster.id 14 | 15 | 16 | class RoundAllKerning(object): 17 | key = "com.harbortype.RoundAllKerning" 18 | windowHeight = 126 19 | padding = (10, 10, 12) 20 | buttonHeight = 20 21 | textHeight = 14 22 | sizeStyle = 'small' 23 | masterOptions = ( 24 | "on currently selected master only", 25 | "on all masters", 26 | ) 27 | 28 | def __init__(self): 29 | 30 | x, y, p = self.padding 31 | 32 | self.w = vanilla.FloatingWindow( 33 | (280, self.windowHeight), 34 | "Round All Kerning" 35 | ) 36 | 37 | y += 8 38 | 39 | # UI elements: 40 | self.w.text_1 = vanilla.TextBox( 41 | (x, y, 216, self.textHeight), 42 | "Round all kerning pairs to multiples of", 43 | sizeStyle=self.sizeStyle 44 | ) 45 | # y += self.textHeight 46 | 47 | # Multiple 48 | formatter = NSNumberFormatter.new() 49 | self.w.multiple = vanilla.EditText( 50 | (216, y - 3, -p, 19), 51 | "10", 52 | sizeStyle=self.sizeStyle, 53 | formatter=formatter, 54 | callback=self.SavePreferences 55 | ) 56 | y += self.textHeight + p - 4 57 | 58 | # Which masters 59 | self.w.whichMasters = vanilla.RadioGroup( 60 | (x * 1.5, y, -p, self.buttonHeight * len(self.masterOptions)), 61 | self.masterOptions, 62 | sizeStyle=self.sizeStyle, 63 | callback=self.SavePreferences, 64 | ) 65 | self.w.whichMasters.getNSMatrix().setToolTip_( 66 | "Choose which font masters shall be affected.") 67 | 68 | y += self.buttonHeight * len(self.masterOptions) 69 | 70 | # Run Button: 71 | self.w.runButton = vanilla.Button( 72 | (80, -20 - 15, -80, -15), 73 | "Round kerning", 74 | sizeStyle='regular', 75 | callback=self.RoundAllKerningMain 76 | ) 77 | self.w.setDefaultButton(self.w.runButton) 78 | 79 | # Load Settings: 80 | if not self.LoadPreferences(): 81 | print( 82 | "Note: 'Round All Kerning' could not load preferences.\ 83 | Will resort to defaults" 84 | ) 85 | 86 | # Open window and focus on it: 87 | self.w.open() 88 | self.w.makeKey() 89 | 90 | def SavePreferences(self, sender): 91 | try: 92 | Glyphs.defaults[self.key + ".multiple"] = self.w.multiple.get() 93 | except: 94 | return False 95 | 96 | return True 97 | 98 | def LoadPreferences(self): 99 | try: 100 | Glyphs.registerDefault(self.key + ".multiple", "10") 101 | Glyphs.registerDefault(self.key + ".whichMasters", 0) 102 | 103 | self.w.multiple.set(Glyphs.defaults[self.key + ".multiple"]) 104 | self.w.whichMasters.set( 105 | bool(Glyphs.defaults[self.key + ".whichMasters"]) 106 | ) 107 | except: 108 | return False 109 | 110 | return True 111 | 112 | def RoundKerningValue(self, kerningValue, base=10): 113 | return int(base * round(float(kerningValue) / base)) 114 | 115 | def GetKey(self, glyph_key): 116 | if glyph_key.startswith("@"): 117 | return glyph_key 118 | return font.glyphForId_(glyph_key).name 119 | 120 | def ProcessMaster(self, master_id): 121 | kerning_dict = font.kerning[master_id] 122 | for left_glyph, right_glyphs in kerning_dict.items(): 123 | left_key = self.GetKey(left_glyph) 124 | for right_glyph, kerning_value in right_glyphs.items(): 125 | right_key = self.GetKey(right_glyph) 126 | rounded_kerning = self.RoundKerningValue( 127 | kerning_value, self.multiple) 128 | if kerning_value != rounded_kerning: 129 | font.setKerningForPair( 130 | master_id, left_key, right_key, rounded_kerning) 131 | 132 | def RoundAllKerningMain(self, sender): 133 | self.multiple = int(self.w.multiple.get()) 134 | self.whichMasters = self.w.whichMasters.get() 135 | 136 | try: 137 | font.disableUpdateInterface() 138 | if self.whichMasters == 1: # all masters 139 | for this_master in font.masters: 140 | self.ProcessMaster(this_master.id) 141 | else: 142 | self.ProcessMaster(font.selectedFontMaster.id) 143 | finally: 144 | font.enableUpdateInterface() 145 | 146 | 147 | RoundAllKerning() 148 | -------------------------------------------------------------------------------- /Paths/Add Extremes to Selection.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Extremes to Selection 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds extreme points to selected paths. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath 9 | 10 | thisFont = Glyphs.font 11 | thisLayer = thisFont.selectedLayers[0] 12 | 13 | try: 14 | for i in range(len(thisLayer.shapes) - 1, -1, -1): 15 | shape = thisLayer.shapes[i] 16 | if isinstance(shape, GSPath): 17 | pathSelected = False 18 | for node in shape.nodes: 19 | if node in thisLayer.selection: 20 | pathSelected = True 21 | break 22 | if pathSelected: 23 | shape.addNodesAtExtremes() 24 | shape.selected = True 25 | except: 26 | for path in thisLayer.paths: 27 | if path.selected: 28 | path.addNodesAtExtremes() 29 | path.selected = True 30 | -------------------------------------------------------------------------------- /Paths/Add Node at 45 degrees.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Nodes at 45° on Selected Segments 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Adds nodes at 45° on selected segments. Interpolation may produce kinks if a node changes the angle AND proportion of its handles between masters. This is not a problem for extremes, but sometimes we need to add intermediate nodes to better control a curve. The easiest way to ensure no kinks will happen in difficult curves is to keep the handles at a constant angle, like 45°. 8 | """ 9 | 10 | from Foundation import NSAffineTransform, NSMakePoint 11 | from GlyphsApp import Glyphs, GSPath, GSNode, OFFCURVE 12 | 13 | Glyphs.clearLog() 14 | font = Glyphs.font 15 | 16 | 17 | def RotatePath(path, angle): 18 | """Rotates a path by an angle in degrees""" 19 | transform = NSAffineTransform.transform() 20 | transform.rotateByDegrees_(angle) 21 | for node in path.nodes: 22 | node.position = transform.transformPoint_( 23 | NSMakePoint(node.x, node.y) 24 | ) 25 | 26 | 27 | thisLayer = font.selectedLayers[0] 28 | for p, thisPath in enumerate(thisLayer.paths): 29 | for n, thisNode in enumerate(thisPath.nodes): 30 | if not thisNode.selected: 31 | continue 32 | nextNode = thisNode.nextNode 33 | if nextNode.type != OFFCURVE: 34 | continue 35 | 36 | # copy nodes 37 | tempPath = GSPath() 38 | for node in thisPath.nodes[n:n + 4]: 39 | newNode = GSNode() 40 | newNode.type = node.type 41 | newNode.smooth = node.smooth 42 | newNode.position = node.position 43 | tempPath.addNode_(newNode) 44 | tempPath.setClosePath_(0) 45 | 46 | RotatePath(tempPath, 45) 47 | tempPath.addNodesAtExtremes() 48 | RotatePath(tempPath, -45) 49 | 50 | newListOfNodes = [] 51 | newListOfNodes.extend(thisPath.nodes[:n]) 52 | newListOfNodes.extend(tempPath.nodes) 53 | newListOfNodes.extend(thisPath.nodes[n + 4:]) 54 | 55 | thisPath.nodes = newListOfNodes 56 | -------------------------------------------------------------------------------- /Paths/Add Point Along Segment.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Point Along Segment 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds points along selected segments at a specific position (time). Needs Vanilla. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs, GSPath 10 | 11 | 12 | class AddPointAlongSegment(object): 13 | 14 | key = "com.harbortype.AddPointAlongSegment" 15 | 16 | def __init__(self): 17 | # Window 'self.w': 18 | windowWidth = 360 19 | windowHeight = 60 20 | windowWidthResize = 200 # user can resize width by this value 21 | windowHeightResize = 0 # user can resize height by this value 22 | self.w = vanilla.FloatingWindow( 23 | (windowWidth, windowHeight), # default window size 24 | "Add Point Along Segment", # window title 25 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 26 | maxSize=(windowWidth + windowWidthResize, windowHeight + \ 27 | windowHeightResize), # maximum size (for resizing) 28 | # stores last window position and size 29 | autosaveName=self.key + ".mainwindow" 30 | ) 31 | 32 | # UI elements: 33 | linePos, inset = 12, 15 34 | buttonWidth = 80 35 | self.w.time = vanilla.Slider( 36 | (inset, linePos, -inset * 3 - buttonWidth - 40, 23), 37 | minValue=0.0, 38 | maxValue=1.0, 39 | tickMarkCount=3, 40 | callback=self.UpdateEditText, 41 | sizeStyle='regular' 42 | ) 43 | self.w.edit_1 = vanilla.EditText( 44 | ( 45 | -inset * 2 - buttonWidth - 40, 46 | linePos, 47 | -inset * 2 - buttonWidth, 48 | 19 49 | ), 50 | sizeStyle='small', 51 | callback=self.UpdateSlider 52 | ) 53 | 54 | # Run Button: 55 | self.w.runButton = vanilla.Button( 56 | (-buttonWidth - inset, linePos + 1, -inset, 17), 57 | "Add point", 58 | sizeStyle='regular', 59 | callback=self.Main 60 | ) 61 | self.w.setDefaultButton(self.w.runButton) 62 | 63 | # Load Settings: 64 | if not self.LoadPreferences(): 65 | print("Note: 'Add Point Along Segment' could not load \ 66 | preferences. Will resort to defaults") 67 | 68 | # Open window and focus on it: 69 | self.w.open() 70 | self.w.makeKey() 71 | 72 | def SavePreferences(self, sender): 73 | try: 74 | Glyphs.defaults[self.domain("time")] = self.w.time.get() 75 | Glyphs.defaults[self.domain("edit_1")] = self.w.edit_1.get() 76 | except: 77 | return False 78 | return True 79 | 80 | def LoadPreferences(self): 81 | try: 82 | Glyphs.registerDefault(self.domain("time"), 0.5) 83 | Glyphs.registerDefault(self.domain("edit_1"), "0.5") 84 | self.w.time.set(Glyphs.defaults[self.domain("time")]) 85 | self.w.edit_1.set(Glyphs.defaults[self.domain("edit_1")]) 86 | except: 87 | return False 88 | return True 89 | 90 | def domain(self, pref_name): 91 | pref_name = pref_name.strip().strip(".") 92 | return self.key + "." + pref_name.strip() 93 | 94 | def UpdateEditText(self, sender): 95 | self.w.edit_1.set(round(self.w.time.get(), 3)) 96 | 97 | def UpdateSlider(self, sender): 98 | newValue = float(self.w.edit_1.get()) 99 | self.w.time.set(newValue) 100 | 101 | def AddPoint(self, pathObject, position): 102 | oldPath = pathObject 103 | oldPathNodes = len(oldPath.nodes) 104 | newPath = oldPath.copy() 105 | addedNodes = 0 106 | for node in pathObject.nodes: 107 | if node.selected and node.prevNode.selected: 108 | nodeIndex = node.index + addedNodes 109 | newPath.insertNodeWithPathTime_(nodeIndex + position) 110 | addedNodes = len(newPath.nodes) - oldPathNodes 111 | return oldPath, newPath 112 | 113 | def Main(self, sender): 114 | try: 115 | # update settings to the latest user input: 116 | if not self.SavePreferences(self): 117 | print("Note: 'Add Point Along Segment' could not write \ 118 | preferences.") 119 | 120 | this_font = Glyphs.font # frontmost font 121 | this_layer = this_font.selectedLayers[0] 122 | position = self.w.time.get() 123 | 124 | if Glyphs.versionNumber >= 3.0: 125 | for i in range(len(this_layer.shapes) - 1, -1, -1): 126 | shape = this_layer.shapes[i] 127 | if isinstance(shape, GSPath): 128 | oldPath, newPath = self.AddPoint(shape, position) 129 | this_layer.removeShape_(oldPath) 130 | this_layer.addShape_(newPath) 131 | else: 132 | for path in this_layer.paths: 133 | oldPath, newPath = self.AddPoint(path, position) 134 | this_layer.removePath_(oldPath) 135 | this_layer.addPath_(newPath) 136 | 137 | except Exception as e: 138 | # brings macro window to front and reports error: 139 | Glyphs.showMacroWindow() 140 | print("Add Point Along Segment Error: %s" % e) 141 | import traceback 142 | print(traceback.format_exc()) 143 | 144 | 145 | AddPointAlongSegment() 146 | -------------------------------------------------------------------------------- /Paths/Create Centerline.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Create Centerline 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates a centerline between two selected paths. The paths should have opposite directions. If it doesn’t work as expected, try reversing one of the paths. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, GSNode, Message, distance 9 | from AppKit import NSMakePoint 10 | 11 | 12 | def pointOnLine(P0, P1, t): 13 | return NSMakePoint(P0.x + ((P1.x - P0.x) * t), P0.y + ((P1.y - P0.y) * t)) 14 | 15 | 16 | def makeCenterline(): 17 | font = Glyphs.font 18 | layer = font.selectedLayers[0] 19 | factor = 0.5 20 | 21 | if not layer.selectedObjects(): 22 | Message( 23 | title="Create Centerline", 24 | message="Please select 2 paths." 25 | ) 26 | return 27 | if Glyphs.versionNumber >= 3.0: 28 | selectedPaths = [shape for shape in layer.selectedObjects( 29 | )["shapes"] if isinstance(shape, GSPath)] 30 | else: 31 | selectedPaths = layer.selectedObjects()["paths"] 32 | 33 | # interpolate paths only if 2 paths are selected: 34 | if (len(selectedPaths) != 2): 35 | Message( 36 | title="Create Centerline", 37 | message="Please select 2 paths." 38 | ) 39 | return 40 | 41 | # check if paths are compatible 42 | if (len(selectedPaths[0].nodes) != len(selectedPaths[1].nodes)): 43 | thisGlyph = layer.parent 44 | Message( 45 | title="Create Centerline", 46 | message="%s: selected paths are not compatible ('%s')." % ( 47 | thisGlyph.name, layer.name 48 | ) 49 | ) 50 | return 51 | 52 | firstPath = selectedPaths[0] 53 | otherPath = selectedPaths[1].copy() 54 | 55 | newPath = interpolatePaths(firstPath, otherPath, factor) 56 | layer.paths.append(newPath) 57 | 58 | 59 | def matchPathDirection(firstPath, otherPath): 60 | nodeDistance = 0 61 | for nodeIndex in range(len(firstPath.nodes)): 62 | thisNode = firstPath.nodes[nodeIndex] 63 | otherNode = otherPath.nodes[nodeIndex] 64 | nodeDistance += distance(thisNode.position, otherNode.position) 65 | otherPath.reverse() 66 | reverseNodeDistance = 0 67 | for nodeIndex in range(len(firstPath.nodes)): 68 | thisNode = firstPath.nodes[nodeIndex] 69 | otherNode = otherPath.nodes[nodeIndex] 70 | reverseNodeDistance += distance(thisNode.position, otherNode.position) 71 | if reverseNodeDistance > nodeDistance: 72 | otherPath.reverse() # reverse it back as it made the match worse 73 | 74 | 75 | def interpolatePaths(firstPath, otherPath, factor): 76 | matchPathDirection(firstPath, otherPath) 77 | newPath = GSPath() 78 | for nodeIndex in range(len(firstPath.nodes)): 79 | thisNode = firstPath.nodes[nodeIndex] 80 | otherNode = otherPath.nodes[nodeIndex] 81 | thisPosition = thisNode.position 82 | otherPosition = otherNode.position 83 | interpolatesPosition = pointOnLine( 84 | thisPosition, otherPosition, factor 85 | ) 86 | newNode = GSNode(interpolatesPosition, thisNode.type) 87 | newNode.connection = thisNode.connection 88 | newNode.roundToGrid_(1) 89 | newPath.nodes.append(newNode) 90 | if firstPath.closed: 91 | newPath.setClosePath_(1) 92 | return newPath 93 | 94 | 95 | makeCenterline() 96 | -------------------------------------------------------------------------------- /Paths/Duplicate Selected Nodes with Offcurve Points.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Duplicate Selected Nodes with Offcurve Points 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates a copy of the selected nodes, adds them in place and create zero-length offcurve points in between. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, GSNode 9 | 10 | font = Glyphs.font 11 | layer = font.selectedLayers[0] 12 | newPathsArray = [] 13 | 14 | 15 | def ProcessPath(path): 16 | newPath = GSPath() 17 | for node in path.nodes: 18 | newPath.addNode_(node.copy()) 19 | if node.type != "offcurve" and node.selected: 20 | newPath.nodes[len(newPath.nodes) - 1].smooth = False 21 | offcurveNode1 = GSNode() 22 | offcurveNode1.type = "offcurve" 23 | offcurveNode1.position = node.position 24 | newPath.addNode_(offcurveNode1) 25 | offcurveNode2 = GSNode() 26 | offcurveNode2.type = "offcurve" 27 | offcurveNode2.position = node.position 28 | newPath.addNode_(offcurveNode2) 29 | newNode = GSNode() 30 | newNode.type = "curve" 31 | newNode.smooth = False 32 | newNode.position = node.position 33 | newPath.addNode_(newNode) 34 | newPath.closed = True if path.closed else False 35 | return newPath 36 | 37 | 38 | layer.beginChanges() 39 | 40 | try: 41 | # Glyphs 3 42 | for i in range(len(layer.shapes) - 1, -1, -1): 43 | shape = layer.shapes[i] 44 | if isinstance(shape, GSPath): 45 | newPath = ProcessPath(shape) 46 | newPathsArray.append(newPath) 47 | else: 48 | newPathsArray.append(shape) 49 | layer.shapes = newPathsArray 50 | except: 51 | # Glyphs 2 52 | for pathIndex, path in enumerate(layer.paths): 53 | newPath = ProcessPath(path) 54 | newPathsArray.append(newPath) 55 | layer.paths = newPathsArray 56 | finally: 57 | layer.endChanges() 58 | -------------------------------------------------------------------------------- /Paths/Duplicate Selected Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Duplicate Selected Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates a copy of the selected nodes and adds them in place 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, GSNode 9 | 10 | font = Glyphs.font 11 | layer = font.selectedLayers[0] 12 | newPathsArray = [] 13 | 14 | 15 | def ProcessPath(path): 16 | newPath = GSPath() 17 | for node in path.nodes: 18 | newPath.addNode_(node.copy()) 19 | if node.type != "offcurve" and node.selected: 20 | newPath.nodes[len(newPath.nodes) - 1].smooth = False 21 | newNode = GSNode() 22 | newNode.type = "line" 23 | newNode.smooth = False 24 | newNode.position = node.position 25 | newPath.addNode_(newNode) 26 | newPath.closed = True if path.closed else False 27 | return newPath 28 | 29 | 30 | layer.beginChanges() 31 | 32 | try: 33 | # Glyphs 3 34 | for i in range(len(layer.shapes) - 1, -1, -1): 35 | shape = layer.shapes[i] 36 | if isinstance(shape, GSPath): 37 | newPath = ProcessPath(shape) 38 | newPathsArray.append(newPath) 39 | else: 40 | newPathsArray.append(shape) 41 | layer.shapes = newPathsArray 42 | except: 43 | # Glyphs 2 44 | for pathIndex, path in enumerate(layer.paths): 45 | newPath = ProcessPath(path) 46 | newPathsArray.append(newPath) 47 | layer.paths = newPathsArray 48 | finally: 49 | layer.endChanges() 50 | -------------------------------------------------------------------------------- /Paths/Interpolate Path with Itself.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Interpolate Path with Itself 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Interpolates the path with itself. The fixed half will be the one with the start point. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath 9 | 10 | # TODO Glyphs 3 compatibility 11 | thisFont = Glyphs.font 12 | allSelected = [x.parent for x in thisFont.selectedLayers[0].selection] 13 | selectedPaths = [] 14 | for selectedItem in allSelected: 15 | if selectedItem not in selectedPaths and isinstance(selectedItem, GSPath): 16 | selectedPaths.append(selectedItem) 17 | 18 | 19 | def interpolateNode(firstNode, secondNode, factor): 20 | interpolatedX = (secondNode.x - firstNode.x) * factor + firstNode.x 21 | interpolatedY = (secondNode.y - firstNode.y) * factor + firstNode.y 22 | interpolatedPosition = [round(interpolatedX), round(interpolatedY)] 23 | return interpolatedPosition 24 | 25 | 26 | for path in selectedPaths: 27 | if not path.closed: 28 | print("Path is not closed.") 29 | else: 30 | # checks if path has an even number of nodes 31 | nodeCount = len(path.nodes) 32 | if nodeCount % 2 == 0: 33 | nodes = path.nodes 34 | # creates node pairs that will be interpolated 35 | nodePairsIndex = [] 36 | for i in range(nodeCount // 2): 37 | nodePairsIndex.append([i - 1, nodeCount - 2 - i]) 38 | # performs the interpolation 39 | for pair in nodePairsIndex: 40 | newPosition = interpolateNode( 41 | nodes[pair[0]], nodes[pair[1]], 0.5) 42 | nodes[pair[1]].x = newPosition[0] 43 | nodes[pair[1]].y = newPosition[1] 44 | -------------------------------------------------------------------------------- /Paths/Make Block Shadow.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make Block Shadow 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Insert points on the tangents at a specific angle 6 | """ 7 | 8 | from Foundation import NSAffineTransform 9 | import vanilla 10 | import math 11 | from GlyphsApp import Glyphs, GSPath, GSNode 12 | 13 | font = Glyphs.font 14 | Glyphs.clearLog() 15 | 16 | 17 | class MakeBlockShadow(object): 18 | 19 | def __init__(self): 20 | 21 | windowWidth = 220 22 | windowHeight = 110 23 | self.w = vanilla.FloatingWindow( 24 | (windowWidth, windowHeight), 25 | "Make Block Shadow", 26 | autosaveName="com.harbortype.MakeBlockShadow.mainwindow" 27 | ) 28 | 29 | self.w.text_1 = vanilla.TextBox((30, 16, 120, 17), "Angle:") 30 | self.w.angle = vanilla.EditText((100, 14, -30, 22), callback=self.SavePreferences) 31 | self.w.text_2 = vanilla.TextBox((30, 46, 120, 17), "Distance:") 32 | self.w.distance = vanilla.EditText((100, 44, -30, 22), callback=self.SavePreferences) 33 | self.w.button = vanilla.Button((30, -34, -30, 20), "Make Shadow", callback=self.Main) 34 | 35 | self.w.setDefaultButton(self.w.button) 36 | 37 | if not self.LoadPreferences(): 38 | print("Note: 'Make Block Shadow' could not load preferences. Will resort to defaults.") 39 | 40 | self.w.open() 41 | self.w.makeKey() 42 | 43 | def SavePreferences(self, sender): 44 | try: 45 | Glyphs.defaults["com.harbortype.MakeBlockShadow.angle"] = self.w.angle.get() 46 | Glyphs.defaults["com.harbortype.MakeBlockShadow.distance"] = self.w.distance.get() 47 | except: 48 | return False 49 | 50 | return True 51 | 52 | def LoadPreferences(self): 53 | try: 54 | Glyphs.registerDefault("com.harbortype.MakeBlockShadow.angle", -45) 55 | Glyphs.registerDefault("com.harbortype.MakeBlockShadow.distance", 100) 56 | self.w.angle.set(Glyphs.defaults["com.harbortype.MakeBlockShadow.angle"]) 57 | self.w.distance.set(Glyphs.defaults["com.harbortype.MakeBlockShadow.distance"]) 58 | except: 59 | return False 60 | 61 | return True 62 | 63 | def RotatePath(self, path, angle): 64 | """Rotates a path by an angle in degrees""" 65 | transform = NSAffineTransform.transform() 66 | transform.rotateByDegrees_(angle) 67 | for node in path.nodes: 68 | node.position = transform.transformPoint_(node.position) 69 | 70 | def Main(self, sender): 71 | font.disableUpdateInterface() 72 | try: 73 | layers = font.selectedLayers 74 | angle = int(self.w.angle.get()) 75 | distance = int(self.w.distance.get()) 76 | 77 | for layer in layers: 78 | 79 | almostExtremes = [] 80 | # Find the tangent nodes 81 | if angle % 90: # if not a right angle 82 | 83 | # Move the origin of the angle to a more natural position for this task 84 | tangentAngle = 90 - angle 85 | 86 | newPaths = [] 87 | for path in layer.paths: 88 | 89 | # Create an empty path 90 | newPath = GSPath() 91 | 92 | for segment in path.segments: 93 | # Create a path from the segment and duplicate it 94 | # so we can compare with the original later on 95 | originalSegment = GSPath() 96 | if Glyphs.versionNumber < 3.0: 97 | for index, point in enumerate(segment): 98 | newNode = GSNode() 99 | newNode.position = point.x, point.y 100 | if index in [1, 2] and len(segment) == 4: 101 | newNode.type = "offcurve" 102 | elif index == 1 and len(segment) == 2: 103 | newNode.type = "line" 104 | else: 105 | newNode.type = "curve" 106 | originalSegment.addNode_(newNode) 107 | else: 108 | for index in range(segment.count()): 109 | newNode = GSNode() 110 | point = segment.pointAtIndex_(index) 111 | newNode.position = point.x, point.y 112 | if index in [1, 2] and segment.count() == 4: 113 | newNode.type = "offcurve" 114 | elif index == 1 and segment.count() == 2: 115 | newNode.type = "line" 116 | else: 117 | newNode.type = "curve" 118 | originalSegment.addNode_(newNode) 119 | fakeSegment = originalSegment.copy() 120 | fakeSegment.nodes[0].type = "line" 121 | 122 | # Rotate the segment and add points to the extremes 123 | self.RotatePath(fakeSegment, tangentAngle) 124 | if Glyphs.versionNumber < 3.0: 125 | fakeSegment.addExtremes_(True) 126 | else: 127 | fakeSegment.addExtremes_checkSelection_(True, False) 128 | 129 | closestNode = None 130 | middleTangent = 1 131 | 132 | # If the segment has 7 nodes, an extreme point was added 133 | if len(fakeSegment) == 7: 134 | # Get the tangent angle of the middle node 135 | middleNode = fakeSegment.nodes[3] 136 | middleTangent = round( 137 | fakeSegment.tangentAngleAtNode_direction_(middleNode, 5) 138 | ) 139 | 140 | elif len(fakeSegment) == 4: 141 | boundsLowX = fakeSegment.bounds.origin.x 142 | boundsHighX = boundsLowX + fakeSegment.bounds.size.width 143 | nodeList = list(fakeSegment.nodes) 144 | startNode = nodeList[0] 145 | endNode = nodeList[-1] 146 | errorMargin = 0.01 147 | 148 | if boundsLowX < startNode.position.x - errorMargin: 149 | if boundsLowX < endNode.position.x - errorMargin: 150 | if startNode.position.x < endNode.position.x: 151 | closestNode = startNode 152 | else: 153 | closestNode = endNode 154 | elif boundsHighX > startNode.position.x + errorMargin: 155 | if boundsHighX > endNode.position.x + errorMargin: 156 | if startNode.position.x > endNode.position.x: 157 | closestNode = startNode 158 | else: 159 | closestNode = endNode 160 | 161 | # Rotate the segment back 162 | self.RotatePath(fakeSegment, -tangentAngle) 163 | 164 | if closestNode: 165 | almostExtremes.append(closestNode.position) 166 | 167 | # If the new diagonal extremes are perpendicular to our angle, 168 | # restore the original segment 169 | if middleTangent % 180 == 0: # check if horizontal 170 | fakeSegment = originalSegment 171 | 172 | # Add the nodes to the new path, skipping the first node 173 | # because the last and first ones repeat on adjacent segments 174 | for node in fakeSegment.nodes[1:]: 175 | newPath.addNode_(node) 176 | 177 | # Close the path (if originally closed) and store it 178 | newPath.closed = True if path.closed else False 179 | newPaths.append(newPath) 180 | else: # if right angle 181 | newPaths = layer.paths 182 | 183 | # Iterate the new paths, which are stored separately 184 | # and were not appended to the layer yet 185 | for path in newPaths: 186 | 187 | # Duplicate the tangent nodes (for extrusion) 188 | 189 | # Get all oncurve nodes 190 | onCurveNodes = [node for node in path.nodes if node.type != "offcurve"] 191 | 192 | # Create a list for our "diagonal extremes" 193 | diagonalExtremes = [] 194 | 195 | # Make the angle positive 196 | tangentAngle = angle 197 | while tangentAngle < 0: 198 | tangentAngle += 360 199 | # Get the angle value from 0° to 180° 200 | tangentAngle = tangentAngle % 180 201 | 202 | for node in onCurveNodes: 203 | errorMargin = 1.5 # in degrees 204 | 205 | if node.position in almostExtremes: 206 | diagonalExtremes.append(node) 207 | 208 | elif node.smooth: # smooth node 209 | # For smooth nodes, check if their tangent angles match ours. 210 | # If true, adds the node to our list of diagonal extremes. 211 | # An error margin is considered. 212 | minTangentAngle = tangentAngle - errorMargin 213 | maxTangentAngle = tangentAngle + errorMargin 214 | nextNodeTan = round(path.tangentAngleAtNode_direction_(node, 1)) 215 | if nextNodeTan < 0: 216 | nextNodeTan += 180 217 | if nextNodeTan > minTangentAngle and nextNodeTan < maxTangentAngle: 218 | diagonalExtremes.append(node) 219 | 220 | else: # corner node 221 | # For non-smooth angles, check if our tangent falls outside 222 | # the angle of the corner. If true, this means this particular 223 | # node will produce a line when extruded. 224 | nextNodeAngle = path.tangentAngleAtNode_direction_(node, 1) 225 | prevNodeAngle = path.tangentAngleAtNode_direction_(node, -1) 226 | # Subtract the tangent angle from the angles of the corner 227 | # then uses sine to check if the angle falls below or above 228 | # the horizontal axis. Only add the node if the angle is 229 | # completely above or below the line. 230 | nextNodeHorizontal = nextNodeAngle - tangentAngle 231 | prevNodeHorizontal = prevNodeAngle - tangentAngle 232 | if math.sin(math.radians(nextNodeHorizontal)) < 0: 233 | if math.sin(math.radians(prevNodeHorizontal)) < 0: 234 | diagonalExtremes.append(node) 235 | if math.sin(math.radians(nextNodeHorizontal)) > 0: 236 | if math.sin(math.radians(prevNodeHorizontal)) > 0: 237 | diagonalExtremes.append(node) 238 | 239 | # Duplicate the diagonal extremes and returns an updated list of nodes 240 | duplicateExtremes = [] 241 | for node in diagonalExtremes: 242 | newNode = GSNode() 243 | newNode.type = "line" 244 | newNode.smooth = False 245 | newNode.position = node.position 246 | node.smooth = False 247 | path.insertNode_atIndex_(newNode, node.index + 1) 248 | duplicateExtremes.append(newNode) 249 | allExtremes = [] 250 | for i in range(len(diagonalExtremes)): 251 | allExtremes.append(diagonalExtremes[i]) 252 | allExtremes.append(duplicateExtremes[i]) 253 | 254 | # Move the nodes 255 | if distance != 0: 256 | 257 | # Calculate the deltaX and deltaY 258 | deltaX = math.cos(math.radians(angle)) * distance 259 | deltaY = math.sin(math.radians(angle)) * distance 260 | 261 | # # Build a list containing all duplicate nodes 262 | # allExtremes = [] 263 | # for node in path.nodes: 264 | # if node.position == node.nextNode.position: 265 | # allExtremes.extend([node, node.nextNode]) 266 | # print(allExtremes) 267 | 268 | # Check if the start point should move or not 269 | fixedStartPoint = True 270 | startNode = path.nodes[-1] 271 | # If the start node is one of the diagonal extremes, use 272 | # the angle we get after subtracting the tangentAngle from 273 | # the secondNodeAngle to determine if the start should be fixed 274 | # or not. It should move if it sits below the horizontal axis. 275 | if startNode in allExtremes: 276 | secondNodeAngle = path.tangentAngleAtNode_direction_(path.nodes[0], 1) 277 | secondNodeHorizontal = secondNodeAngle - tangentAngle 278 | if math.sin(math.radians(secondNodeHorizontal)) < 0: 279 | fixedStartPoint = False 280 | # If the start node is not a diagonal extreme, duplicate the 281 | # path and move it by 1 unit in the direction of the extrusion. 282 | else: 283 | # Get the NSBezierPath and move it 284 | offsetPath = path.bezierPath 285 | translate = NSAffineTransform.transform() 286 | translate.translateXBy_yBy_( 287 | math.copysign(1, deltaX), 288 | math.copysign(1, deltaY)) 289 | offsetPath.transformUsingAffineTransform_(translate) 290 | 291 | startPoint = startNode.position 292 | # On counterclockwise (filled) paths, the start node should 293 | # move if the start node falls INSIDE the transformed path 294 | if path.direction == -1: 295 | if offsetPath.containsPoint_(startPoint): 296 | fixedStartPoint = False 297 | # On clockwise paths, the start node should move if the 298 | # start node falls OUTSIDE the transformed path 299 | elif path.direction == 1: 300 | if not offsetPath.containsPoint_(startPoint): 301 | fixedStartPoint = False 302 | 303 | # If the start point should move, rearrange 304 | # the list containing the diagonal extremes 305 | if not fixedStartPoint: 306 | if path.nodes[-1] not in diagonalExtremes: 307 | if diagonalExtremes[-1].index > diagonalExtremes[0].index: 308 | lastNode = diagonalExtremes.pop(-1) 309 | diagonalExtremes.insert(0, lastNode) 310 | 311 | # Only move if the number diagonal extremes is even 312 | if len(diagonalExtremes) % 2 == 0: 313 | n = 0 314 | tupleList = [] 315 | for i in range(len(diagonalExtremes) // 2): 316 | tupleList.append((diagonalExtremes[n], diagonalExtremes[n + 1])) 317 | n += 2 318 | 319 | for pair in tupleList: 320 | if pair[0].index > pair[1].index: 321 | selection = path.nodes[pair[0].index + 1:] 322 | selection.extend(path.nodes[: pair[1].index + 1]) 323 | else: 324 | selection = path.nodes[pair[0].index + 1:pair[1].index + 1] 325 | 326 | # Finaly move a node 327 | for node in selection: 328 | pos = node.position 329 | pos.x = pos.x + deltaX 330 | pos.y = pos.y + deltaY 331 | node.position = pos 332 | else: 333 | print("Could not find all extremes for glyph", layer.parent.name) 334 | 335 | # Replace all paths with the new ones 336 | if newPaths: 337 | if Glyphs.versionNumber < 3.0: 338 | layer.paths = newPaths 339 | else: 340 | layer.shapes = newPaths 341 | layer.roundCoordinates() 342 | layer.selection = None 343 | 344 | except Exception as e: 345 | import traceback 346 | print(traceback.format_exc()) 347 | raise e 348 | 349 | finally: 350 | font.enableUpdateInterface() 351 | 352 | 353 | MakeBlockShadow() 354 | -------------------------------------------------------------------------------- /Paths/Make Next Node First.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make Next Node First 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Moves the start point of the selected path(s) to the next oncurve node. Specially useful if assigned to a keyboard shortcut. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath 9 | 10 | thisFont = Glyphs.font 11 | 12 | # Gets the parent object of every selected element. This return a large 13 | # list, with many duplicate items, so we'll need to filter it later. 14 | allSelected = [x.parent for x in thisFont.selectedLayers[0].selection] 15 | 16 | # Loops through previous list, removes all duplicate items keeping only 17 | # objects that are paths. 18 | selectedPaths = [] 19 | for selectedItem in allSelected: 20 | if selectedItem not in selectedPaths and isinstance(selectedItem, GSPath): 21 | selectedPaths.append(selectedItem) 22 | 23 | # For each selected path, gets a list of oncurve nodes and makes 24 | # the secondmost node the first one. 25 | for path in selectedPaths: 26 | onCurveNodes = [node for node in path.nodes if node.type != "offcurve"] 27 | onCurveNodes[0].makeNodeFirst() 28 | -------------------------------------------------------------------------------- /Paths/Make Previous Node First.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make Previous Node First 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Moves the start point of the selected path(s) to the previous oncurve node. Specially useful if assigned to a keyboard shortcut. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath 9 | 10 | thisFont = Glyphs.font 11 | currentMaster = thisFont.selectedFontMaster 12 | 13 | # Gets the parent object of every selected element. This return a large 14 | # list, with many duplicate items, so we'll need to filter it later. 15 | allSelected = [x.parent for x in thisFont.selectedLayers[0].selection] 16 | 17 | # Loops through previous list, removes all duplicate items keeping only 18 | # objects that are paths. 19 | selectedPaths = [] 20 | for selectedItem in allSelected: 21 | if selectedItem not in selectedPaths and isinstance(selectedItem, GSPath): 22 | selectedPaths.append(selectedItem) 23 | 24 | # For each selected path, gets a list of oncurve nodes and makes 25 | # the last node the first one. 26 | for path in selectedPaths: 27 | onCurveNodes = [node for node in path.nodes if node.type != "offcurve"] 28 | onCurveNodes[-2].makeNodeFirst() 29 | -------------------------------------------------------------------------------- /Paths/New Tab with Overlaps.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Overlaps 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new Edit tab containing all glyphs that contain overlaps. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | from AppKit import NSClassFromString 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | master_ids = [master.id for master in thisFont.masters] # all the master ids 13 | 14 | 15 | def check_for_overlaps(lyr): 16 | paths = list(lyr.paths) 17 | GSPathOperator = NSClassFromString("GSPathOperator") 18 | segments = GSPathOperator.segmentsFromPaths_(paths) 19 | count1 = len(segments) 20 | if Glyphs.versionNumber >= 3.0: 21 | GSPathOperator.addIntersections_(segments) 22 | else: 23 | PathOperator = GSPathOperator.new() 24 | PathOperator.addIntersections_(segments) 25 | count2 = len(segments) 26 | if count1 != count2: 27 | return True 28 | return False 29 | 30 | 31 | try: 32 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 33 | text = "" 34 | for thisGlyph in thisFont.glyphs: 35 | thisLayer = thisGlyph.layers[0] 36 | if not thisLayer.paths: 37 | continue 38 | if thisLayer.layerId in master_ids or thisLayer.isSpecialLayer: 39 | has_overlaps = check_for_overlaps(thisLayer) 40 | if has_overlaps: 41 | text += "/%s " % (thisGlyph.name) 42 | if text: 43 | thisFont.newTab(text) 44 | else: 45 | Message( 46 | title="New Tab with Overlaps", 47 | message="No glyphs with overlaps in this font.", 48 | ) 49 | finally: 50 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 51 | -------------------------------------------------------------------------------- /Paths/Open All Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Open All Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens all nodes for the selected paths (or all paths if none are selected). 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, GSNode 9 | 10 | font = Glyphs.font 11 | layers = font.selectedLayers 12 | 13 | 14 | def open_nodes(path): 15 | 16 | for segment in path.segments: 17 | 18 | try: # Glyphs 3 19 | countOfPoints = segment.countOfPoints() 20 | except: # Glyphs 2 21 | countOfPoints = len(segment) 22 | 23 | newPath = GSPath() 24 | if countOfPoints == 4: 25 | for n in range(countOfPoints): 26 | point = segment[n] 27 | newNode = GSNode() 28 | newNode.position = point.x, point.y 29 | if n == 0: 30 | newNode.type = 'line' 31 | elif n == 3: 32 | newNode.type = 'curve' 33 | else: 34 | newNode.type = 'offcurve' 35 | newPath.addNode_(newNode) 36 | else: 37 | for n in range(countOfPoints): 38 | point = segment[n] 39 | newNode = GSNode() 40 | newNode.position = point.x, point.y 41 | newNode.type = 'line' 42 | newPath.addNode_(newNode) 43 | 44 | newPaths.append(newPath) 45 | 46 | 47 | for layer in layers: 48 | 49 | try: 50 | layer.beginChanges() 51 | 52 | selection = [x.parent for x in layer.selection] 53 | selectedPaths = [] 54 | for item in selection: 55 | if item not in selectedPaths and isinstance(item, GSPath): 56 | selectedPaths.append(item) 57 | if len(selectedPaths) == 0: 58 | selectedPaths = layer.paths 59 | 60 | newPaths = [] 61 | try: # Glyphs 3 62 | for i in range(len(layer.shapes) - 1, -1, -1): 63 | shape = layer.shapes[i] 64 | if isinstance(shape, GSPath) and shape in selectedPaths: 65 | open_nodes(shape) 66 | del layer.shapes[i] 67 | layer.shapes.extend(newPaths) 68 | except: # Glyphs 2 69 | for i in range(len(layer.paths) - 1, -1, -1): 70 | path = layer.paths[i] 71 | if path in selectedPaths: 72 | open_nodes(path) 73 | del layer.paths[i] 74 | layer.paths.extend(newPaths) 75 | 76 | finally: 77 | layer.endChanges() 78 | -------------------------------------------------------------------------------- /Paths/Open Selected Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Open Selected Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens the selected nodes. Originally written by Luisa Leitenperger. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, OFFCURVE 9 | 10 | font = Glyphs.font 11 | layer = font.selectedLayers[0] 12 | 13 | try: 14 | layer.beginChanges() 15 | 16 | pathArray = [] 17 | delArray = [] 18 | 19 | for pathIndex, path in enumerate(layer.paths): 20 | 21 | # Make a list of selected nodes as long as they're not handles 22 | # (handles can't be split open) 23 | selectedNodes = [] 24 | for node in path.nodes: 25 | if node.selected and node.type != OFFCURVE: 26 | selectedNodes.append(node) 27 | 28 | if len(selectedNodes) > 0: 29 | 30 | for nodeIndex, node in enumerate(selectedNodes): 31 | 32 | # Create a new path for each node on the list and 33 | # add copies of every node until the next selected node. 34 | newPath = GSPath() 35 | 36 | # IF PATH IS CLOSED 37 | if path.closed: 38 | 39 | # If it's the last (or only) node on the list, 40 | # copy every node until the end of the path then restart 41 | # at the beginning of the path until it reaches the first 42 | # node on the list. 43 | if node == selectedNodes[-1]: 44 | 45 | for newNode in path.nodes[node.index:]: 46 | newPath.addNode_(newNode.copy()) 47 | 48 | # If the first node of the list is also the first node 49 | # of the path, only that first node will be included, 50 | # no need for loops. 51 | if selectedNodes[0].index == 0: 52 | newPath.addNode_(path.nodes[0].copy()) 53 | 54 | else: 55 | for newNode in path.nodes[0:selectedNodes[0].index + 1]: 56 | newPath.addNode_(newNode.copy()) 57 | 58 | else: 59 | for newNode in path.nodes[node.index:selectedNodes[nodeIndex + 1].index + 1]: 60 | newPath.addNode_(newNode.copy()) 61 | 62 | # IF PATH IS OPEN 63 | else: 64 | 65 | # Create a path for the first nodes 66 | if nodeIndex == 0 and path.nodes[node.index].index > 0: 67 | secondPath = GSPath() 68 | for newNode in path.nodes[: node.index + 1]: 69 | secondPath.addNode_(newNode.copy()) 70 | pathArray.append(secondPath) 71 | 72 | # If the node is the last one selected, 73 | # add all nodes until the end of the path 74 | if node == selectedNodes[-1]: 75 | for newNode in path.nodes[node.index:]: 76 | newPath.addNode_(newNode.copy()) 77 | 78 | # Otherwise, add nodes until the next selected one 79 | else: 80 | for newNode in path.nodes[node.index:selectedNodes[nodeIndex + 1].index + 1]: 81 | newPath.addNode_(newNode.copy()) 82 | 83 | # An open path has to start with a line node 84 | newPath.nodes[0].type = "line" 85 | 86 | pathArray.append(newPath) 87 | 88 | # Flag path to be deleted if there are selected nodes on that path 89 | delArray.append(pathIndex) 90 | 91 | # Delete original paths so there are no duplicates 92 | for pathIndex in reversed(delArray): 93 | try: # Glyphs 3 94 | del layer.shapes[pathIndex] 95 | except: # Glyphs 2 96 | del layer.paths[pathIndex] 97 | 98 | # Append the open paths 99 | for path in pathArray: 100 | try: # Glyphs 3 101 | layer.shapes.append(path) 102 | except: # Glyphs 2 103 | layer.paths.append(path) 104 | 105 | finally: 106 | layer.endChanges() 107 | -------------------------------------------------------------------------------- /Paths/Re-interpolate.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Re-interpolate 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Re-interpolates selected layers. Makes it possible to assign a keyboard shortcut to this command via Preferences > Shortcuts (in Glyphs 3) or System Preferences > Keyboard > Shortcuts > App Shortcuts (in Glyphs 2). 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font 11 | for thisLayer in thisFont.selectedLayers: 12 | thisLayer.reinterpolate() 13 | -------------------------------------------------------------------------------- /Paths/Remove Overlaps and Correct Path Directions.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Overlaps and Correct Path Directions in All Masters 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Removes overlaps (if so, copies the original to the background), corrects path directions in all layers and opens a new tab with glyphs that became incompatible. Applies to the selected glyphs only. Reports in Macro Window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSControlLayer 9 | from AppKit import NSClassFromString 10 | 11 | 12 | def checkForOverlaps(lyr): 13 | if not lyr.paths: 14 | return False 15 | paths = list(lyr.paths) 16 | GSPathOperator = NSClassFromString("GSPathOperator") 17 | segments = GSPathOperator.segmentsFromPaths_(paths) 18 | count1 = len(segments) 19 | if Glyphs.versionNumber >= 3.0: 20 | GSPathOperator.addIntersections_(segments) 21 | else: 22 | PathOperator = GSPathOperator.new() 23 | PathOperator.addIntersections_(segments) 24 | count2 = len(segments) 25 | if count1 != count2: 26 | return True 27 | return False 28 | 29 | 30 | def process(lyr): 31 | lyr.setBackground_(lyr) 32 | lyr.removeOverlap() 33 | lyr.correctPathDirection() 34 | 35 | 36 | thisFont = Glyphs.font # frontmost font 37 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 38 | master_ids = [master.id for master in thisFont.masters] # all the master ids 39 | 40 | # Glyphs.clearLog() # clears log in Macro window 41 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 42 | 43 | for thisLayer in selectedLayers: 44 | 45 | if isinstance(thisLayer, GSControlLayer): 46 | continue 47 | 48 | thisGlyph = thisLayer.parent 49 | thisGlyph.beginUndo() # begin undo grouping 50 | if checkForOverlaps(thisGlyph.layers[0]): 51 | for layer in thisGlyph.layers: 52 | if layer.layerId in master_ids or layer.isSpecialLayer: 53 | print("Processing %s : %s" % (thisGlyph.name, layer.name)) 54 | process(layer) 55 | else: 56 | for layer in thisGlyph.layers: 57 | if layer.layerId in master_ids or layer.isSpecialLayer: 58 | print("Correcting path directions for %s : %s" % 59 | (thisGlyph.name, layer.name)) 60 | layer.correctPathDirection() 61 | thisGlyph.endUndo() # end undo grouping 62 | 63 | text = "" 64 | for thisLayer in selectedLayers: 65 | thisGlyph = thisLayer.parent 66 | if thisGlyph.mastersCompatible: 67 | continue 68 | else: 69 | if "/%s " % (thisGlyph.name) not in text: 70 | text += "/%s " % (thisGlyph.name) 71 | 72 | if text: 73 | thisFont.newTab(text) 74 | 75 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 76 | -------------------------------------------------------------------------------- /Paths/Round Coordinates for All Layers.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Round Coordinates for All Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Round coordinates of all paths in all layers of the current glyph. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | from Foundation import NSPoint 10 | 11 | 12 | def round_path_coordinates(this_path): 13 | for this_node in this_path.nodes: 14 | x = int(this_node.position.x) 15 | y = int(this_node.position.y) 16 | # Glyphs will only set the position if there is 17 | # a difference larger than 1/100 of a unit, so we 18 | # force a larger difference of 1 unit and then 19 | # set it back to the original coordinates. 20 | # https://forum.glyphsapp.com/t/roundcoordinates-doesnt-round-all-coordinates/10936/4 21 | this_node.position = NSPoint(x + 1, y + 1) 22 | this_node.position = NSPoint(x, y) 23 | 24 | 25 | this_font = Glyphs.font 26 | 27 | try: 28 | this_font.disableUpdateInterface() 29 | for selected_layer in this_font.selectedLayers: 30 | for this_layer in selected_layer.parent.layers: 31 | for this_path in this_layer.paths: 32 | round_path_coordinates(this_path) 33 | # Round the layer width as well 34 | if not this_layer.width.is_integer(): 35 | this_layer.width = round(this_layer.width) 36 | finally: 37 | this_font.enableUpdateInterface() 38 | -------------------------------------------------------------------------------- /Paths/Round Coordinates for Entire Font.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Round Coordinates for Entire Font 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Round coordinates of all paths in the entire font. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | from Foundation import NSPoint 10 | 11 | this_font = Glyphs.font 12 | 13 | try: 14 | this_font.disableUpdateInterface() 15 | for this_glyph in this_font.glyphs: 16 | for this_layer in this_glyph.layers: 17 | # Round all nodes 18 | for this_path in this_layer.paths: 19 | for this_node in this_path.nodes: 20 | x = int(this_node.position.x) 21 | y = int(this_node.position.y) 22 | # Glyphs will only set the position if there is 23 | # a difference larger than 1/100 of a unit, so we 24 | # force a larger difference of 1 unit and then 25 | # set it back to the original coordinates. 26 | # https://forum.glyphsapp.com/t/roundcoordinates-doesnt-round-all-coordinates/10936/4 27 | this_node.position = NSPoint(x + 1, y + 1) 28 | this_node.position = NSPoint(x, y) 29 | # Round the layer width as well 30 | if not this_layer.width.is_integer(): 31 | this_layer.width = round(this_layer.width) 32 | finally: 33 | this_font.enableUpdateInterface() 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glyphs scripts 2 | 3 | An assortment of scripts for the [Glyphs font editor](http://glyphsapp.com/). 4 | 5 | ## Script list 6 | 7 | ### Anchors 8 | 9 | - **Add Caret Anchors:** Adds caret_* anchors to the selected glyphs based from the base glyphs’ widths. Applies to all layers. Does not modify existing caret anchors. 10 | - **Copy Layer Anchors from Other Font:** Copies the anchors of the current layer from the background file. 11 | - **Copy Selected Anchors from Other Font:** Copies the position of the selected anchors from the background file. 12 | - **Re-interpolate Anchors:** Re-interpolates only the anchors on selected layers. 13 | 14 | ### Color Fonts 15 | 16 | - **SVG Export and SVG Import:** Generates SVGs from inside Glyphs and reimports them for creating SVG color fonts. More information below. Needs Vanilla and Drawbot. 17 | 18 | ### Components 19 | 20 | - **Decompose Nested Components:** Decompose nested components on selected glyphs. 21 | - **Make Component Glyph across All Layers:** Assembles the selected glyphs from components (the same as running the Make Component Glyph command for all layers) and removes all anchors. 22 | - **New Tab with Components in Background:** Opens a new Edit tab with glyphs containing components in their backgrounds. 23 | - **New Tab with Nested Components:** Opens a new Edit tab with glyphs that contain components made of components. 24 | - **Rebuild Components in Double Quotes:** Replaces components in double quotes using the single quotes in all layers. For example, if the quotedblleft is made from a rotated quotedblright, it will copy the current component to the background and rebuild it using 2 quotelefts. 25 | - **Report Components Vertical Position:** Reports the y coordinate of all components in the selected glyphs. 26 | - **Set Components Alignment Type 3:** Sets the automatic alignment of all components in the selected glyphs to be type 3 (the type that allows to be vertically shifted). Applies to all masters. 27 | 28 | ### Font 29 | 30 | - **Export Fonts into Subfolder:** Exports OTF and TTF at the same time into a specified subfolder. Needs Vanilla. 31 | - **Remove Vertical Metrics Parameters from Instances:** Removes all vertical metrics parameters from instances (typo, hhea and win). 32 | - **Reorder Axes:** Reorder axes and their values in masters, instances and special layers. Needs Vanilla. 33 | - **Replace in Family Name:** Finds and replaces in family name, including Variable Font Family Name and instances’ familyName custom parameters. Needs Vanilla. 34 | - **Sort Instances:** Sorts instances by axes values. Needs Vanilla. 35 | 36 | ### Glyph Names 37 | 38 | - **Copy Sort Names from Background Font:** Copies the custom sortNames for all glyphs from the font in the background. 39 | - **List Glyphs in Current Tab:** Appends a line with the unique glyphs of the current tab. 40 | - **Rename Glyphs and Update Features:** Renames glyphs and updates all classes and features. Will match either the entire glyph name or the dot suffix. Needs Vanilla. 41 | 42 | ### Hinting 43 | 44 | - **Export Hinting Test Page (HTML):** Create a Test HTML for the current font inside the current Webfont Export folder, or for the current Glyphs Project in the project’s export path. 45 | Based on mekkablue's Webfont Test HTML script. 46 | - **New Tab with Rotated, Scaled or Flipped Components:** Opens a new edit tab with components that were rotated, scaled or flipped. They may cause issues on TrueType. 47 | - **New Tab with Vertically Shifted Components:** Opens a new edit tab with components that are transformed beyond mere horizontal shifts. 48 | - **Reverse PS Hint:** Reverses the direction of the selected PS hints. 49 | 50 | ### Interpolation 51 | 52 | - **New Tab with Repeating Components and Paths:** Opens a new Edit tab with glyphs that contain multiple instances of the same component or path. They might be interpolating with the wrong ones! 53 | 54 | ### Layers 55 | 56 | - **Copy Master into Sublayer:** Copies a master into a sublayer of another master for the selected glyphs. Useful for creating COLR/CPAL color fonts. Based on [@mekkablue](https://github.com/mekkablue/Glyphs-Scripts)'s Copy Layer to Layer script. Needs Vanilla. 57 | - **Remove all layers for the current master:** Deletes all non-master layers for the current master (including bracket and brace layers) in selected glyphs. 58 | 59 | ### Metrics and Kerning 60 | 61 | - **Copy Kerning Groups from Unsuffixed Glyphs:** Copies the kerning groups from the default (unsuffixed) glyphs to the selected ones. The selected glyphs need to have a dot suffix, otherwise they will be skipped. 62 | - **New Tab with Kerning Exceptions:** Opens a new Edit tab containing all kerning exceptions for the current master. 63 | - **New Tab with Kerning Pairs for Selected Glyph:** Opens a new tab with kerning pairs for the selected glyph (minus diacritics). 64 | - **New Tab with Missing Kerning Pairs:** Compares two glyphs files and opens a new tab with missing kerning pairs for the current master. 65 | - **New Tab with Zero Kerning Pairs:** Opens a new tab with missing kerning pairs (value set as zero) for each master. 66 | - **Round all Kerning:** Rounds all kerning pairs of the current master (or all masters) according a multiple provided by you. 67 | 68 | ### Paths 69 | 70 | - **Add Extremes to Selection:** Adds extreme points to selected paths. 71 | - **Add Nodes on Selected Segments at 45°:** Adds nodes at 45° on selected segments. Interpolation may produce kinks if a node changes the angle AND proportion of its handles between masters. This is not a problem for extremes, but sometimes we need to add intermediate nodes to better control a curve. The easiest way to ensure no kinks will happen in difficult curves is to keep the handles at a constant angle, like 45°. 72 | - **Add Point Along Segment:** Adds points along selected segments at a specific position (time). Needs Vanilla. 73 | - **Create Centerline:** Creates a centerline between two selected paths. The paths should have opposite directions. If it doesn’t work as expected, try reversing one of the paths. Needs Vanilla. 74 | - **Duplicate Selected Nodes:** Creates a copy of the selected nodes and adds them in place. 75 | - **Duplicate Selected Nodes with Offcurve Points:** Creates a copy of the selected nodes, adds them in place and create zero-length offcurve points in between. 76 | - **Interpolate Path with Itself:** Interpolates the path with itself. The fixed half will be the one with the start point. This script will be improved in the near future. 77 | - **Make Block Shadow:** Insert points on the tangents at a specific angle and extrude the path in the same direction, creating a block shadow effect. Needs Vanilla. 78 | - **Make Next Node First:** Moves the start point of the selected path(s) to the next oncurve node. Specially useful if assigned to a keyboard shortcut. 79 | - **Make Previous Node First:** Moves the start point of the selected path(s) to the previous oncurve node. Specially useful if assigned to a keyboard shortcut. 80 | - **New Tab with Overlaps:** Opens a new Edit tab containing all glyphs that contain overlaps. 81 | - **Open All Nodes:** Opens all nodes for the selected paths (or all paths if none are selected). 82 | - **Open Selected Nodes:** Opens the selected nodes. 83 | - **Re-interpolate:** Re-interpolates selected layers. Makes it possible to assign a keyboard shortcut to this command via Preferences > Shortcuts (in Glyphs 3) or System Preferences > Keyboard > Shortcuts > App Shortcuts (in Glyphs 2). 84 | - **Remove Overlaps and Correct Path Directions in All Masters:** Removes overlaps (if so, copies the original to the background), corrects path directions in all layers and opens a new tab with glyphs that became incompatible. Reports in Macro Window. 85 | - **Round Coordinates for Entire Font:** Round coordinates of all paths in the entire font. 86 | 87 | ## SVG Export and Import 88 | 89 | ### SVG Export.py 90 | 91 | 1) Before running the script, define which masters should be exported and their colors in JSON files and place them alongside your .glyphs file. Let’s call the following example `acquamarine.json`: 92 | 93 | ```json 94 | { 95 | "Extrude": [ 15, 54, 69 ], 96 | "Regular": [ 1, 187, 226 ], 97 | "Bevel B": { 98 | "startPoint": [0,0], 99 | "endPoint": [0,550], 100 | "colors": [ 101 | [ 168, 231, 244 ], 102 | [ 195, 241, 249 ] 103 | ], 104 | "locations": [0,1] 105 | } 106 | } 107 | ``` 108 | 109 | I believe the template above is quite self-explanatory. `Extrude`, `Regular` and `Bevel B` are names masters in our .glyphs file. Extrude and Regular have a solid fill, while Bevel B has a linear gradient fill. All trio of values represent RGB colors. You may add more colors as necessary. If you do so, remember to include additional “stops” in `locations`. Zero represents the start point of the gradient and 1 is the end point. 110 | 111 | 2) Run the script and type the name of the JSON file on the window. The script will create a subfolder with the same name and place all SVGs inside it. 112 | 113 | ### SVG Import.py 114 | 115 | 1) On a copy of your original .glyphs file, create a master for each one of the color schemes files you exported with the previous script. For example, if you have a subfolder called “acquamarine”, create a master named “Acquamarine” and assign to it a custom axis value. The name is case-insensitive. 116 | 117 | 2) After creating all masters, create an instance for each one of them so they will be exported. To do so, got o **Font Info** > **Instances** > **+** > **Add Instance for each Master**. 118 | 119 | 3) Run the script. 120 | 121 | In a nutshell, for each master, it will check if there is a subfolder with the same name as the master, create a layer named *svg* and import the SVG file into it. 122 | 123 | 124 | ## Installing 125 | 126 | ### Glyphs 3 127 | 128 | 1. **Install the modules:** In *Window > Plugin Manager,* click on the *Modules* tab, and make sure at least the [Python](glyphsapp3://showplugin/python) and [Vanilla](glyphsapp3://showplugin/vanilla) modules are installed. If they were not installed before, restart the app. 129 | 2. **Install the scripts:** Go to *Window > Plugin Manager* and click on the *Scripts* tab. Scroll down to [Harbortype scripts](glyphsapp3://showplugin/harbortype%20scripts%20by%20Henrique%20Beier) and click on the *Install* button next to it. 130 | 131 | Now the scripts are available in *Script > Harbortype.* If the Harbortype scripts do not show up in the *Script* menu, hold down the Option key and choose *Script > Reload Scripts* (⌘⌥⇧Y). 132 | 133 | ### Glyphs 2 134 | 135 | Download or clone this repository and put it into the Scripts folder. To find this folder, go to Script > Open Scripts Folder (⌘⇧Y) in Glyphs. After placing the scripts there, hold down the Option key and go to Script > Reload Scripts (⌘⌥⇧Y). The scripts will now be visible in the Script menu. 136 | 137 | Some scripts require Tal Leming’s Vanilla. To install it, go to the menu Glyphs > Preferences > Addons > Modules and click the Install Modules button. 138 | 139 | ## License 140 | 141 | Copyright 2018 Henrique Beier (@harbortype). Some code samples by Rainer Erich Scheichelbauer (@mekkablue) and Luisa Leitenperger. 142 | 143 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 144 | 145 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 146 | 147 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 148 | --------------------------------------------------------------------------------