├── Align ├── Align Bottom.py ├── Align Horizontal Center.py ├── Align Left.py ├── Align Right.py ├── Align Top.py ├── Align Vertical Center.py ├── Center Selected Glyphs.py ├── Distribute Nodes Horizontally.py └── Distribute Nodes Vertically.py ├── App ├── DarkMode.py ├── Export To All Formats.py ├── Fit Zoom.py ├── Floating Macro Panel.py ├── Next Layer in Selection.py ├── Previous Layer in Selection.py ├── Toggle Axis 1.py ├── Toggle Axis 2.py ├── Toggle Axis 3.py ├── Toggle Axis 4.py ├── Toggle Italic.py └── ToggleAxis.py ├── Bug fixing ├── Dangerous Offcurves.py └── Point Counter.py ├── Features └── Generate Random Alternates Feature.py ├── Font Info ├── Demo Instance Generator.py ├── Preferred Names - Clean.py ├── Preferred Names - Set.py ├── Set Vertical Metrics.py └── glyphOrder - Paste.py ├── G2 Harmonize.py ├── Glyphs ├── Copy Missing Special Layers.py ├── Reorder Shapes.py └── Show All Layers.py ├── Kerning ├── Delete Kerning Pair From All Masters.py ├── Kern to max.py └── Steal Kerning From Next Pair at Cursor's Y.py ├── LICENSE ├── Overlap nodes.py ├── README.md ├── Reflect ├── Reflect Horizontally.py └── Reflect Vertically.py ├── Rotate ├── Rotate 1 CCW.py ├── Rotate 1 CW.py ├── Rotate 10 CCW.py ├── Rotate 10 CW.py ├── Rotate 45 CCW.py ├── Rotate 45 CW.py └── Rotate.py └── Testing ├── Highest & Lowest Glyphs.py └── Text Filter.py /Align/Align Bottom.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Bottom 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the bottom of the selection or nearest metrics. 7 | """ 8 | 9 | from math import radians, tan 10 | from Foundation import NSPoint, NSEvent 11 | from GlyphsApp import Glyphs, GSComponent, GSNode 12 | 13 | DIRECTION = 'Bottom' # either 'Top' or 'Bottom' 14 | 15 | 16 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 17 | try: 18 | oldRange = (oldMax - oldMin) 19 | newRange = (newMax - newMin) 20 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 21 | return newValue 22 | except: 23 | return None 24 | 25 | 26 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 27 | # get the smooth line of nodes 28 | line = [] 29 | 30 | if element.smooth: 31 | # 4 nodes line 32 | if prev.smooth or next.smooth: 33 | if prev.smooth: 34 | line = [prevPrev, prev, element, next] 35 | elif next.smooth: 36 | line = [prev, element, next, nextNext] 37 | # 3 nodes line 38 | else: 39 | line = [prev, element, next] 40 | 41 | elif prev.smooth: 42 | if prevPrev.smooth: 43 | line = [prevPrevPrev, prevPrev, prev, element] 44 | else: 45 | line = [prevPrev, prev, element] 46 | elif next.smooth: 47 | if nextNext.smooth: 48 | line = [nextNextNext, nextNext, next, element] 49 | else: 50 | line = [nextNext, next, element] 51 | return line 52 | 53 | 54 | def keepSmooth(element, currentY): 55 | try: 56 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 57 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 58 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 59 | selectedInLine = [] 60 | except: 61 | line = [] 62 | 63 | if line: 64 | for node in line: 65 | if node.selected: 66 | selectedInLine.append(node) 67 | 68 | # align everything if more than 2 nodes in line are selected 69 | if len(selectedInLine) > 1: 70 | # unless they all have the same x 71 | for node in line: 72 | if node.selected or node.x != element.x: 73 | node.y = element.y 74 | 75 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 76 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 77 | pass 78 | # for node in line: 79 | # if node != element: 80 | # node.y += (element.y - currentY) 81 | 82 | # keep smooth if line len == 3 and one offcurve is not selected 83 | elif ( 84 | len(line) == 3 85 | and ( 86 | (line[0].type == 'offcurve' and line[0].selected is False) 87 | or (line[2].type == 'offcurve' and line[2].selected is False) 88 | ) 89 | ): 90 | if (line[0].type == 'offcurve' and line[0].selected is False): 91 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 92 | else: 93 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 94 | 95 | # keep smooth if line len == 4 and only one oncurve is selected 96 | elif (len(line) == 4 and len(selectedInLine) == 1): 97 | if line[1].selected or line[2].selected: 98 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 99 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 100 | 101 | # otherwise adjust Y 102 | else: 103 | for node in line: 104 | if node != line[0] and node != line[-1]: 105 | if element == line[0]: 106 | 107 | newY = remap(node.y, currentY, line[-1].y, element.y, line[-1].y) 108 | node.y = newY 109 | elif element == line[-1]: 110 | newY = remap(node.y, currentY, line[0].y, element.y, line[0].y) 111 | node.y = newY 112 | 113 | 114 | # from @mekkablue snippets 115 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): 116 | x = thisPoint.x 117 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 118 | italicAngle = radians(italicAngle) # convert to radians 119 | tangens = tan(italicAngle) # math.tan needs radians 120 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 121 | x += horizontalDeviance # x of point that is yOffset from pivotal point 122 | return NSPoint(int(x), thisPoint.y) 123 | 124 | 125 | # ---------------------------------------- 126 | 127 | 128 | layer = Glyphs.currentFont.selectedLayers[0] 129 | selection = layer.selection 130 | 131 | 132 | def getSelectedPaths(): 133 | selectedPaths = [] 134 | for path in layer.paths: 135 | if path.selected: 136 | selectedPaths.append(path) 137 | return selectedPaths 138 | 139 | 140 | selectedPaths = getSelectedPaths() 141 | 142 | 143 | def alignToGuides(): 144 | # get guides and alignment zones 145 | if Glyphs.versionNumber < 3: # Glyphs 2 146 | layerWidth, ascender, capHeight, descender, xHeight, italicAngle, unclear, midXheight = layer.glyphMetrics() 147 | descenderZone = layer.master.alignmentZoneForMetric_(layer.master.descender) 148 | baselineZone = layer.master.alignmentZoneForMetric_(0) 149 | xHeightZone = layer.master.alignmentZoneForMetric_(layer.master.xHeight) 150 | capHeightZone = layer.master.alignmentZoneForMetric_(layer.master.capHeight) 151 | ascenderZone = layer.master.alignmentZoneForMetric_(layer.master.ascender) 152 | # Known bug: special layers get zones from masters. Not sure how to access layer’s zones 153 | else: # Glyphs 3 154 | ascenderZone, capHeightZone, xHeightZone, baselineZone, descenderZone = layer.metrics 155 | ascender, capHeight, xHeight, baseline, descender = ascenderZone.position, capHeightZone.position, xHeightZone.position, baselineZone.position, descenderZone.position 156 | guides = [descender, 0, xHeight, capHeight, ascender, int(xHeight / 2), int((xHeight + capHeight) / 4), int(capHeight / 2)] 157 | 158 | if descenderZone: 159 | guides.append(descender + descenderZone.size) 160 | if baselineZone: 161 | guides.append(baselineZone.size) 162 | if xHeightZone: 163 | guides.append(xHeight + xHeightZone.size) 164 | if capHeightZone: 165 | guides.append(capHeight + capHeightZone.size) 166 | if ascenderZone: 167 | guides.append(ascender + ascenderZone.size) 168 | guides.sort() 169 | 170 | if len(selection) == 1: 171 | # prev next oncurves as guides 172 | node = selection[0] 173 | try: 174 | guideNode = None 175 | if node.prevNode.type != 'offcurve': 176 | guideNode = node.prevNode 177 | elif node.prevNode.prevNode.prevNode != 'offcurve': 178 | guideNode = node.prevNode.prevNode.prevNode 179 | if guideNode and guideNode.y not in guides and node != guideNode: 180 | guides.append(guideNode.y) 181 | except: pass 182 | try: 183 | guideNode = None 184 | if node.nextNode.type != 'offcurve': 185 | guideNode = node.nextNode 186 | elif node.nextNode.nextNode.nextNode != 'offcurve': 187 | guideNode = node.nextNode.nextNode.nextNode 188 | if guideNode and guideNode.y not in guides and node != guideNode: 189 | guides.append(guideNode.y) 190 | except: pass 191 | guides.sort() 192 | 193 | # get closest guide and shiftY 194 | if DIRECTION == 'Bottom': 195 | currentY = layer.selectionBounds.origin.y 196 | closestGuide = guides[0] 197 | elif DIRECTION == 'Top': 198 | currentY = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 199 | closestGuide = guides[-1] 200 | 201 | for guide in guides: 202 | if DIRECTION == 'Bottom': 203 | if guide < currentY and currentY - guide < currentY - closestGuide: 204 | closestGuide = guide 205 | elif DIRECTION == 'Top': 206 | if guide > currentY and guide - currentY < closestGuide - currentY: 207 | closestGuide = guide 208 | 209 | shiftY = closestGuide - currentY 210 | # align 211 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 212 | for node in selection: 213 | node.y += shiftY 214 | if italicAngle != 0: 215 | node.x = italicize(node, italicAngle, node.y - shiftY).x 216 | # keep smooth 217 | if len(selection) == 1: 218 | try: 219 | keepSmooth(selection[0], currentY) 220 | except: pass 221 | 222 | 223 | def alignToSelection(): 224 | for element in selection: 225 | # align components 226 | if isinstance(element, GSComponent): 227 | y = int(element.bounds.origin.y - element.y) # Glyphs 2 and 3 have different x y of components 228 | if DIRECTION == 'Bottom': 229 | element.y = layer.selectionBounds.origin.y - y 230 | elif DIRECTION == 'Top': 231 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height - element.bounds.size.height - y 232 | 233 | # align nodes 234 | elif isinstance(element, GSNode): 235 | align = True 236 | if selectedPaths: 237 | for path in selectedPaths: 238 | if element in path.nodes: 239 | align = False 240 | break 241 | 242 | if align is True: 243 | currentY = element.y 244 | if DIRECTION == 'Bottom': 245 | element.y = layer.selectionBounds.origin.y 246 | elif DIRECTION == 'Top': 247 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 248 | keepSmooth(element, currentY) 249 | 250 | # align anchors 251 | else: 252 | if DIRECTION == 'Bottom': 253 | element.y = layer.selectionBounds.origin.y 254 | elif DIRECTION == 'Top': 255 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 256 | # align paths 257 | if selectedPaths: 258 | for path in selectedPaths: 259 | if DIRECTION == 'Bottom': 260 | # get highest node in the path 261 | lowest = None 262 | for node in path.nodes: 263 | if lowest is None: 264 | lowest = node.y 265 | elif node.y < lowest: 266 | lowest = node.y 267 | shift = layer.selectionBounds.origin.y - lowest 268 | elif DIRECTION == 'Top': 269 | highest = None 270 | for node in path.nodes: 271 | if highest is None: 272 | highest = node.y 273 | elif node.y > highest: 274 | highest = node.y 275 | shift = layer.selectionBounds.origin.y + layer.selectionBounds.size.height - highest 276 | 277 | path.applyTransform(( 278 | 1, # x scale factor 279 | 0, # x skew factor 280 | 0, # y skew factor 281 | 1, # y scale factor 282 | 0, # x position 283 | shift # y position 284 | )) 285 | 286 | 287 | # see if all selected points share Y coordinate 288 | sameY = True 289 | for element in selection: 290 | if element.y != selection[0].y: 291 | sameY = False 292 | break 293 | 294 | # in caps lock mode, selection aligns to guides 295 | cpsKeyFlag = 65536 296 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 297 | 298 | # if there’s only one element or path, align it to the guides 299 | # or caps lock is on 300 | if ( 301 | len(selection) == 1 302 | or sameY is True 303 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 304 | or cpsPressed 305 | ): 306 | alignToGuides() 307 | 308 | # if more than one element is selected 309 | else: 310 | alignToSelection() 311 | 312 | # update metrics 313 | layer.updateMetrics() 314 | 315 | 316 | 317 | ''' 318 | smooth variations 319 | 320 | possible combinations 321 | 322 | OFFCURVE - smooth - offcurve 323 | OFFCURVE - SMOOTH - offcurve 324 | OFFCURVE - smooth - OFFCURVE 325 | offcurve - SMOOTH - offcurve 326 | offcurve - SMOOTH - OFFCURVE 327 | offcurve - smooth - OFFCURVE 328 | 329 | ONCURVE - smooth - offcurve 330 | ONCURVE - SMOOTH - offcurve 331 | ONCURVE - smooth - OFFCURVE 332 | oncurve - SMOOTH - offcurve 333 | oncurve - SMOOTH - OFFCURVE 334 | oncurve - smooth - OFFCURVE 335 | 336 | OFFCURVE - smooth - oncurve 337 | OFFCURVE - SMOOTH - oncurve 338 | OFFCURVE - smooth - ONCURVE 339 | offcurve - SMOOTH - oncurve 340 | offcurve - SMOOTH - ONCURVE 341 | offcurve - smooth - ONCURVE 342 | 343 | OFFCURVE - smooth - smooth - offcurve 344 | OFFCURVE - SMOOTH - smooth - offcurve 345 | OFFCURVE - SMOOTH - SMOOTH - offcurve 346 | offcurve - SMOOTH - SMOOTH - OFFCURVE 347 | offcurve - smooth - SMOOTH - OFFCURVE 348 | offcurve - smooth - smooth - OFFCURVE 349 | 350 | OFFCURVE - smooth - SMOOTH - offcurve 351 | offcurve - SMOOTH - smooth - OFFCURVE 352 | offcurve - smooth - SMOOTH - offcurve 353 | offcurve - SMOOTH - smooth - offcurve 354 | 355 | align everything if: 356 | more than 2 selected 357 | OFFCURVE - SMOOTH - offcurve 358 | offcurve - SMOOTH - OFFCURVE 359 | OFFCURVE - smooth - OFFCURVE 360 | ONCURVE - SMOOTH - offcurve 361 | oncurve - SMOOTH - OFFCURVE 362 | ONCURVE - smooth - OFFCURVE 363 | OFFCURVE - SMOOTH - oncurve 364 | offcurve - SMOOTH - ONCURVE 365 | OFFCURVE - smooth - ONCURVE 366 | OFFCURVE - SMOOTH - smooth - offcurve 367 | OFFCURVE - SMOOTH - SMOOTH - offcurve 368 | offcurve - SMOOTH - SMOOTH - OFFCURVE 369 | offcurve - smooth - SMOOTH - OFFCURVE 370 | OFFCURVE - smooth - SMOOTH - offcurve 371 | offcurve - SMOOTH - smooth - OFFCURVE 372 | 373 | keep smooth if: 374 | if line lenght 3 and one offcurve is not selected 375 | OFFCURVE - smooth - offcurve 376 | offcurve - smooth - OFFCURVE 377 | ONCURVE - smooth - offcurve 378 | offcurve - smooth - ONCURVE 379 | 380 | offcurve - SMOOTH - oncurve 381 | oncurve - SMOOTH - offcurve 382 | 383 | recalc Ys if: 384 | if line lenght 3 and 1 oncurve is not selected or line lenght 4 and only 1 offcurve is selected 385 | OFFCURVE - smooth - oncurve 386 | oncurve - smooth - OFFCURVE 387 | OFFCURVE - smooth - smooth - offcurve 388 | offcurve - smooth - smooth - OFFCURVE 389 | 390 | 391 | push everything if: 392 | only one node is selected and it's smooth and the other node is not non smooth offcurve 393 | offcurve - SMOOTH - offcurve 394 | offcurve - smooth - SMOOTH - offcurve 395 | offcurve - SMOOTH - smooth - offcurve 396 | ''' 397 | -------------------------------------------------------------------------------- /Align/Align Horizontal Center.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Horizontal Center 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the horizontal center of the selection or nearest metrics. 7 | """ 8 | 9 | from math import radians, tan 10 | from Foundation import NSPoint, NSEvent 11 | from GlyphsApp import Glyphs, GSComponent, GSNode 12 | 13 | 14 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 15 | try: 16 | oldRange = (oldMax - oldMin) 17 | newRange = (newMax - newMin) 18 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 19 | return newValue 20 | except: 21 | return None 22 | 23 | 24 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 25 | # get the smooth line of nodes 26 | line = [] 27 | 28 | if element.smooth: 29 | # 4 nodes line 30 | if prev.smooth or next.smooth: 31 | if prev.smooth: 32 | line = [prevPrev, prev, element, next] 33 | elif next.smooth: 34 | line = [prev, element, next, nextNext] 35 | # 3 nodes line 36 | else: 37 | line = [prev, element, next] 38 | 39 | elif prev.smooth: 40 | if prevPrev.smooth: 41 | line = [prevPrevPrev, prevPrev, prev, element] 42 | else: 43 | line = [prevPrev, prev, element] 44 | elif next.smooth: 45 | if nextNext.smooth: 46 | line = [nextNextNext, nextNext, next, element] 47 | else: 48 | line = [nextNext, next, element] 49 | return line 50 | 51 | 52 | def keepSmooth(element, currentX): 53 | try: 54 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 55 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 56 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 57 | selectedInLine = [] 58 | except: 59 | line = [] 60 | 61 | if line: 62 | for node in line: 63 | if node.selected: 64 | selectedInLine.append(node) 65 | 66 | # align everything if more than 2 nodes in line are selected 67 | if len(selectedInLine) > 1: 68 | # unless they all have the same y 69 | for node in line: 70 | if node.selected or node.y != element.y: 71 | node.x = element.x 72 | 73 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 74 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 75 | pass 76 | # for node in line: 77 | # if node != element: 78 | # node.x += (element.x - currentX) 79 | 80 | # keep smooth if line len == 3 and one offcurve is not selected 81 | elif ( 82 | len(line) == 3 83 | and ( 84 | (line[0].type == 'offcurve' and line[0].selected is False) 85 | or (line[2].type == 'offcurve' and line[2].selected is False) 86 | ) 87 | ): 88 | if (line[0].type == 'offcurve' and line[0].selected is False): 89 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 90 | else: 91 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 92 | 93 | # keep smooth if line len == 4 and only one oncurve is selected 94 | elif (len(line) == 4 and len(selectedInLine) == 1): 95 | if line[1].selected or line[2].selected: 96 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 97 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 98 | 99 | # otherwise adjust X 100 | else: 101 | for node in line: 102 | if node != line[0] and node != line[-1]: 103 | if element == line[0]: 104 | 105 | newX = remap(node.x, currentX, line[-1].x, element.x, line[-1].x) 106 | node.x = newX 107 | elif element == line[-1]: 108 | newX = remap(node.x, currentX, line[0].x, element.x, line[0].x) 109 | node.x = newX 110 | 111 | 112 | # from @mekkablue snippets 113 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): # don't change x to y for horizontal / vertical DIRECTION 114 | x = thisPoint.x 115 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 116 | italicAngle = radians(italicAngle) # convert to radians 117 | tangens = tan(italicAngle) # math.tan needs radians 118 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 119 | x += horizontalDeviance # x of point that is yOffset from pivotal point 120 | return NSPoint(int(x), thisPoint.y) 121 | 122 | 123 | # ---------------------------------------- 124 | 125 | 126 | layer = Glyphs.font.selectedLayers[0] 127 | selection = layer.selection 128 | 129 | 130 | def getSelectedPaths(): 131 | selectedPaths = [] 132 | for path in layer.paths: 133 | if path.selected: 134 | selectedPaths.append(path) 135 | return selectedPaths 136 | 137 | 138 | selectedPaths = getSelectedPaths() 139 | 140 | 141 | def alignToGuides(): 142 | # collect guides 143 | guides = [int(layer.width / 2)] 144 | guides.sort() 145 | 146 | # check italic 147 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 148 | if italicAngle != 0: 149 | # italicize guides 150 | italicGuides = [] 151 | for guide in guides: 152 | italicGuide = italicize(NSPoint(guide, 0), italicAngle, layer.master.xHeight / 2)[0] 153 | italicGuides.append(italicGuide) 154 | italicGuides.sort() 155 | guides = italicGuides 156 | 157 | # < backslant layer 158 | ySkew = tan(radians(italicAngle)) 159 | layer.applyTransform(( 160 | 1.0, # x scale factor 161 | 0.0, # x skew factor 162 | -ySkew, # y skew factor 163 | 1.0, # y scale factor 164 | 0.0, # x position 165 | 0.0 # y position 166 | )) 167 | 168 | # get closest guide and shiftX 169 | currentX = layer.selectionBounds.origin.x + (layer.selectionBounds.size.width / 2) 170 | for i, guide in enumerate(guides): 171 | if currentX < guide: 172 | closestGuide = guide 173 | break 174 | elif currentX >= guides[-1]: 175 | closestGuide = guides[0] 176 | shiftX = closestGuide - currentX 177 | 178 | # align to the guide 179 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 180 | for element in selection: 181 | element.x += shiftX 182 | 183 | # set smooth 184 | if len(selection) == 1: 185 | try: 186 | keepSmooth(selection[0], currentX) 187 | except: pass 188 | 189 | # if italic, slant back 190 | if italicAngle != 0: 191 | ySkew = tan(radians(italicAngle)) 192 | layer.applyTransform(( 193 | 1.0, # x scale factor 194 | 0.0, # x skew factor 195 | ySkew, # y skew factor 196 | 1.0, # y scale factor 197 | 0.0, # x position 198 | 0.0 # y position 199 | )) 200 | 201 | 202 | def alignToSelectionCenter(): 203 | selectionCenter = layer.selectionBounds.origin.x + layer.selectionBounds.size.width / 2 204 | for element in selection: 205 | # align components 206 | if isinstance(element, GSComponent): 207 | x = element.bounds.origin.x - element.x # Glyphs 2 and 3 have different x y of components 208 | element.x = selectionCenter - element.bounds.size.width / 2 - x 209 | 210 | # align nodes 211 | elif isinstance(element, GSNode): 212 | align = True 213 | if selectedPaths: 214 | for path in selectedPaths: 215 | if element in path.nodes: 216 | align = False 217 | break 218 | 219 | if align is True: 220 | currentX = element.x 221 | element.x = selectionCenter 222 | keepSmooth(element, currentX) 223 | 224 | # align anchors 225 | else: 226 | element.x = selectionCenter 227 | 228 | # align paths 229 | if selectedPaths: 230 | for path in selectedPaths: 231 | pathCenter = path.bounds.origin.x + path.bounds.size.width / 2 232 | shiftX = selectionCenter - pathCenter 233 | 234 | path.applyTransform(( 235 | 1, # x scale factor 236 | 0.0, # x skew factor 237 | 0.0, # y skew factor 238 | 1, # y scale factor 239 | shiftX, # x position 240 | 0 # y position 241 | )) 242 | 243 | 244 | # see if all selected points share Y coordinate 245 | sameX = True 246 | for element in selection: 247 | if element.x != selection[0].x: 248 | sameX = False 249 | break 250 | 251 | # in caps lock mode, selection aligns to guides 252 | cpsKeyFlag = 65536 253 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 254 | 255 | # if there’s only one element or path, align it to the guides 256 | # or caps lock is on 257 | if ( 258 | len(selection) == 1 259 | or sameX is True 260 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 261 | or cpsPressed 262 | ): 263 | alignToGuides() 264 | 265 | # if more than one element is selected 266 | else: 267 | alignToSelectionCenter() 268 | 269 | # update metrics 270 | layer.updateMetrics() 271 | -------------------------------------------------------------------------------- /Align/Align Left.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Left 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the left of the selection or nearest metrics. 7 | """ 8 | 9 | from math import radians, tan 10 | from Foundation import NSPoint, NSEvent 11 | from GlyphsApp import Glyphs, GSComponent, GSNode 12 | 13 | DIRECTION = 'Left' # either 'Left' or 'Right' 14 | 15 | 16 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 17 | try: 18 | oldRange = (oldMax - oldMin) 19 | newRange = (newMax - newMin) 20 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 21 | return newValue 22 | except: 23 | return None 24 | 25 | 26 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 27 | # get the smooth line of nodes 28 | line = [] 29 | 30 | if element.smooth: 31 | # 4 nodes line 32 | if prev.smooth or next.smooth: 33 | if prev.smooth: 34 | line = [prevPrev, prev, element, next] 35 | elif next.smooth: 36 | line = [prev, element, next, nextNext] 37 | # 3 nodes line 38 | else: 39 | line = [prev, element, next] 40 | 41 | elif prev.smooth: 42 | if prevPrev.smooth: 43 | line = [prevPrevPrev, prevPrev, prev, element] 44 | else: 45 | line = [prevPrev, prev, element] 46 | elif next.smooth: 47 | if nextNext.smooth: 48 | line = [nextNextNext, nextNext, next, element] 49 | else: 50 | line = [nextNext, next, element] 51 | return line 52 | 53 | 54 | def keepSmooth(element, currentX): 55 | try: 56 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 57 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 58 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 59 | selectedInLine = [] 60 | except: 61 | line = [] 62 | 63 | if line: 64 | for node in line: 65 | if node.selected: 66 | selectedInLine.append(node) 67 | 68 | # align everything if more than 2 nodes in line are selected 69 | if len(selectedInLine) > 1: 70 | # unless they all have the same y 71 | for node in line: 72 | if node.selected or node.y != element.y: 73 | node.x = element.x 74 | 75 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 76 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 77 | pass 78 | # for node in line: 79 | # if node != element: 80 | # node.x += (element.x - currentX) 81 | 82 | # keep smooth if line len == 3 and one offcurve is not selected 83 | elif ( 84 | len(line) == 3 85 | and ( 86 | (line[0].type == 'offcurve' and line[0].selected is False) 87 | or (line[2].type == 'offcurve' and line[2].selected is False) 88 | ) 89 | ): 90 | if (line[0].type == 'offcurve' and line[0].selected is False): 91 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 92 | else: 93 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 94 | 95 | # keep smooth if line len == 4 and only one oncurve is selected 96 | elif ( 97 | len(line) == 4 98 | and len(selectedInLine) == 1 99 | ): 100 | if line[1].selected or line[2].selected: 101 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 102 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 103 | 104 | # otherwise adjust X 105 | else: 106 | for node in line: 107 | if node != line[0] and node != line[-1]: 108 | if element == line[0]: 109 | 110 | newX = remap(node.x, currentX, line[-1].x, element.x, line[-1].x) 111 | node.x = newX 112 | elif element == line[-1]: 113 | newX = remap(node.x, currentX, line[0].x, element.x, line[0].x) 114 | node.x = newX 115 | 116 | 117 | # from @mekkablue snippets 118 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): # don't change x to y for horizontal / vertical DIRECTION 119 | x = thisPoint.x 120 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 121 | italicAngle = radians(italicAngle) # convert to radians 122 | tangens = tan(italicAngle) # math.tan needs radians 123 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 124 | x += horizontalDeviance # x of point that is yOffset from pivotal point 125 | return NSPoint(int(x), thisPoint.y) 126 | 127 | 128 | # ---------------------------------------- 129 | 130 | 131 | layer = Glyphs.font.selectedLayers[0] 132 | selection = layer.selection 133 | 134 | 135 | def getSelectedPaths(): 136 | selectedPaths = [] 137 | for path in layer.paths: 138 | if path.selected: 139 | selectedPaths.append(path) 140 | return selectedPaths 141 | 142 | 143 | selectedPaths = getSelectedPaths() 144 | 145 | 146 | def alignToGuides(): 147 | # collect guides 148 | metricGuides = [0, int(layer.width / 2), layer.width] # check this for italic guides 149 | guides = [] + metricGuides 150 | if len(selection) == 1: 151 | # prev next oncurves as guides 152 | node = selection[0] 153 | nodeGuides = [] 154 | try: 155 | guideNode = None 156 | if node.prevNode.type != 'offcurve': 157 | guideNode = node.prevNode 158 | elif node.prevNode.prevNode.prevNode != 'offcurve': 159 | guideNode = node.prevNode.prevNode.prevNode 160 | if guideNode and guideNode.x not in guides and node != guideNode: 161 | guides.append(guideNode.x) 162 | nodeGuides.append(guideNode) 163 | except: pass 164 | try: 165 | guideNode = None 166 | if node.nextNode.type != 'offcurve': 167 | guideNode = node.nextNode 168 | elif node.nextNode.nextNode.nextNode != 'offcurve': 169 | guideNode = node.nextNode.nextNode.nextNode 170 | if guideNode and guideNode.x not in guides and node != guideNode: 171 | guides.append(guideNode.x) 172 | nodeGuides.append(guideNode) 173 | except: pass 174 | guides.sort() 175 | 176 | # check italic 177 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 178 | if italicAngle != 0: 179 | # italicize guides 180 | italicGuides = [] 181 | for guide in guides: 182 | if guide in metricGuides: 183 | italicGuide = italicize(NSPoint(guide, 0), italicAngle, layer.master.xHeight / 2)[0] 184 | else: 185 | for nodeGuide in nodeGuides: 186 | if guide == nodeGuide.x: 187 | italicGuide = italicize(NSPoint(guide, 0), italicAngle, nodeGuide.y)[0] 188 | italicGuides.append(italicGuide) 189 | italicGuides.sort() 190 | guides = italicGuides 191 | 192 | # < backslant layer 193 | ySkew = tan(radians(italicAngle)) 194 | layer.applyTransform(( 195 | 1.0, # x scale factor 196 | 0.0, # x skew factor 197 | -ySkew, # y skew factor 198 | 1.0, # y scale factor 199 | 0.0, # x position 200 | 0.0 # y position 201 | )) 202 | 203 | # get closest guide 204 | if DIRECTION == 'Left': 205 | currentX = layer.selectionBounds.origin.x 206 | closestGuide = guides[0] 207 | for guide in guides: 208 | if guide < currentX and currentX - guide < currentX - closestGuide: 209 | closestGuide = guide 210 | 211 | elif DIRECTION == 'Right': 212 | currentX = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 213 | closestGuide = guides[-1] 214 | for guide in guides: 215 | if guide > currentX and guide - currentX < closestGuide - currentX: 216 | closestGuide = guide 217 | 218 | # align to the guide 219 | shiftX = closestGuide - currentX 220 | for node in selection: 221 | node.x += shiftX 222 | 223 | # set smooth 224 | if len(selection) == 1: 225 | try: 226 | keepSmooth(selection[0], currentX) 227 | except: pass 228 | 229 | # if italic, slant back 230 | if italicAngle != 0: 231 | ySkew = tan(radians(italicAngle)) 232 | layer.applyTransform(( 233 | 1.0, # x scale factor 234 | 0.0, # x skew factor 235 | ySkew, # y skew factor 236 | 1.0, # y scale factor 237 | 0.0, # x position 238 | 0.0 # y position 239 | )) 240 | 241 | 242 | def alignToSelection(): 243 | for element in selection: 244 | # align components 245 | if isinstance(element, GSComponent): 246 | x = int(element.bounds.origin.x - element.x) # Glyphs 2 and 3 have different x y of components 247 | if DIRECTION == 'Left': 248 | element.x = layer.selectionBounds.origin.x - x 249 | elif DIRECTION == 'Right': 250 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width - element.bounds.size.width - x 251 | 252 | # align nodes 253 | elif isinstance(element, GSNode): 254 | align = True 255 | if selectedPaths: 256 | for path in selectedPaths: 257 | if element in path.nodes: 258 | align = False 259 | break 260 | 261 | if align is True: 262 | currentX = element.x 263 | if DIRECTION == 'Left': 264 | element.x = layer.selectionBounds.origin.x 265 | elif DIRECTION == 'Right': 266 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 267 | keepSmooth(element, currentX) 268 | 269 | # align anchors 270 | else: 271 | if DIRECTION == 'Left': 272 | element.x = layer.selectionBounds.origin.x 273 | elif DIRECTION == 'Right': 274 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 275 | 276 | # align paths 277 | if selectedPaths: 278 | for path in selectedPaths: 279 | if DIRECTION == 'Left': 280 | # get highest node in the path 281 | leftest = None 282 | for node in path.nodes: 283 | if leftest is None: 284 | leftest = node.x 285 | elif node.x < leftest: 286 | leftest = node.x 287 | shiftX = layer.selectionBounds[0].x - leftest 288 | elif DIRECTION == 'Right': 289 | rightest = None 290 | for node in path.nodes: 291 | if rightest is None: 292 | rightest = node.x 293 | elif node.x > rightest: 294 | rightest = node.x 295 | shiftX = layer.selectionBounds[0].x + layer.selectionBounds[1].width - rightest 296 | 297 | path.applyTransform(( 298 | 1, # x scale factor 299 | 0, # x skew factor 300 | 0, # y skew factor 301 | 1, # y scale factor 302 | shiftX, # x position 303 | 0 # y position 304 | )) 305 | 306 | 307 | # see if all selected points share Y coordinate 308 | sameX = True 309 | for element in selection: 310 | if element.x != selection[0].x: 311 | sameX = False 312 | break 313 | 314 | # in caps lock mode, selection aligns to guides 315 | cpsKeyFlag = 65536 316 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 317 | 318 | # if there’s only one element or path, align it to the guides 319 | # or caps lock is on 320 | if ( 321 | len(selection) == 1 322 | or sameX is True 323 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 324 | or cpsPressed 325 | ): 326 | alignToGuides() 327 | 328 | # if more than one element is selected 329 | else: 330 | alignToSelection() 331 | 332 | # update metrics 333 | layer.updateMetrics() 334 | 335 | ''' 336 | smooth variations 337 | 338 | possible combinations 339 | 340 | OFFCURVE - smooth - offcurve 341 | OFFCURVE - SMOOTH - offcurve 342 | OFFCURVE - smooth - OFFCURVE 343 | offcurve - SMOOTH - offcurve 344 | offcurve - SMOOTH - OFFCURVE 345 | offcurve - smooth - OFFCURVE 346 | 347 | ONCURVE - smooth - offcurve 348 | ONCURVE - SMOOTH - offcurve 349 | ONCURVE - smooth - OFFCURVE 350 | oncurve - SMOOTH - offcurve 351 | oncurve - SMOOTH - OFFCURVE 352 | oncurve - smooth - OFFCURVE 353 | 354 | OFFCURVE - smooth - oncurve 355 | OFFCURVE - SMOOTH - oncurve 356 | OFFCURVE - smooth - ONCURVE 357 | offcurve - SMOOTH - oncurve 358 | offcurve - SMOOTH - ONCURVE 359 | offcurve - smooth - ONCURVE 360 | 361 | OFFCURVE - smooth - smooth - offcurve 362 | OFFCURVE - SMOOTH - smooth - offcurve 363 | OFFCURVE - SMOOTH - SMOOTH - offcurve 364 | offcurve - SMOOTH - SMOOTH - OFFCURVE 365 | offcurve - smooth - SMOOTH - OFFCURVE 366 | offcurve - smooth - smooth - OFFCURVE 367 | 368 | OFFCURVE - smooth - SMOOTH - offcurve 369 | offcurve - SMOOTH - smooth - OFFCURVE 370 | offcurve - smooth - SMOOTH - offcurve 371 | offcurve - SMOOTH - smooth - offcurve 372 | 373 | 374 | 375 | align everything if: 376 | more than 2 selected 377 | OFFCURVE - SMOOTH - offcurve 378 | offcurve - SMOOTH - OFFCURVE 379 | OFFCURVE - smooth - OFFCURVE 380 | ONCURVE - SMOOTH - offcurve 381 | oncurve - SMOOTH - OFFCURVE 382 | ONCURVE - smooth - OFFCURVE 383 | OFFCURVE - SMOOTH - oncurve 384 | offcurve - SMOOTH - ONCURVE 385 | OFFCURVE - smooth - ONCURVE 386 | OFFCURVE - SMOOTH - smooth - offcurve 387 | OFFCURVE - SMOOTH - SMOOTH - offcurve 388 | offcurve - SMOOTH - SMOOTH - OFFCURVE 389 | offcurve - smooth - SMOOTH - OFFCURVE 390 | OFFCURVE - smooth - SMOOTH - offcurve 391 | offcurve - SMOOTH - smooth - OFFCURVE 392 | 393 | keep smooth if: 394 | if line lenght 3 and one offcurve is not selected 395 | OFFCURVE - smooth - offcurve 396 | offcurve - smooth - OFFCURVE 397 | ONCURVE - smooth - offcurve 398 | offcurve - smooth - ONCURVE 399 | 400 | offcurve - SMOOTH - oncurve 401 | oncurve - SMOOTH - offcurve 402 | 403 | recalc Ys if: 404 | if line lenght 3 and 1 oncurve is not selected or line lenght 4 and only 1 offcurve is selected 405 | OFFCURVE - smooth - oncurve 406 | oncurve - smooth - OFFCURVE 407 | OFFCURVE - smooth - smooth - offcurve 408 | offcurve - smooth - smooth - OFFCURVE 409 | 410 | 411 | push everything if: 412 | only one node is selected and it's smooth and the other node is not non smooth offcurve 413 | offcurve - SMOOTH - offcurve 414 | offcurve - smooth - SMOOTH - offcurve 415 | offcurve - SMOOTH - smooth - offcurve 416 | ''' 417 | -------------------------------------------------------------------------------- /Align/Align Right.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Right 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the right of the selection or nearest metrics. 7 | """ 8 | from math import radians, tan 9 | from Foundation import NSPoint, NSEvent 10 | from GlyphsApp import Glyphs, GSComponent, GSNode 11 | 12 | DIRECTION = 'Right' # either 'Left' or 'Right' 13 | 14 | 15 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 16 | try: 17 | oldRange = (oldMax - oldMin) 18 | newRange = (newMax - newMin) 19 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 20 | return newValue 21 | except: 22 | return None 23 | 24 | 25 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 26 | # get the smooth line of nodes 27 | line = [] 28 | 29 | if element.smooth: 30 | # 4 nodes line 31 | if prev.smooth or next.smooth: 32 | if prev.smooth: 33 | line = [prevPrev, prev, element, next] 34 | elif next.smooth: 35 | line = [prev, element, next, nextNext] 36 | # 3 nodes line 37 | else: 38 | line = [prev, element, next] 39 | 40 | elif prev.smooth: 41 | if prevPrev.smooth: 42 | line = [prevPrevPrev, prevPrev, prev, element] 43 | else: 44 | line = [prevPrev, prev, element] 45 | elif next.smooth: 46 | if nextNext.smooth: 47 | line = [nextNextNext, nextNext, next, element] 48 | else: 49 | line = [nextNext, next, element] 50 | return line 51 | 52 | 53 | def keepSmooth(element, currentX): 54 | try: 55 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 56 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 57 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 58 | selectedInLine = [] 59 | except: 60 | line = [] 61 | 62 | if line: 63 | for node in line: 64 | if node.selected: 65 | selectedInLine.append(node) 66 | 67 | # align everything if more than 2 nodes in line are selected 68 | if len(selectedInLine) > 1: 69 | # unless they all have the same y 70 | for node in line: 71 | if node.selected or node.y != element.y: 72 | node.x = element.x 73 | 74 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 75 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 76 | pass 77 | # for node in line: 78 | # if node != element: 79 | # node.x += (element.x - currentX) 80 | 81 | # keep smooth if line len == 3 and one offcurve is not selected 82 | elif ( 83 | len(line) == 3 84 | and ( 85 | (line[0].type == 'offcurve' and line[0].selected is False) 86 | or (line[2].type == 'offcurve' and line[2].selected is False) 87 | ) 88 | ): 89 | if (line[0].type == 'offcurve' and line[0].selected is False): 90 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 91 | else: 92 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 93 | 94 | # keep smooth if line len == 4 and only one oncurve is selected 95 | elif ( 96 | len(line) == 4 97 | and len(selectedInLine) == 1 98 | ): 99 | if line[1].selected or line[2].selected: 100 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 101 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 102 | 103 | # otherwise adjust X 104 | else: 105 | for node in line: 106 | if node != line[0] and node != line[-1]: 107 | if element == line[0]: 108 | 109 | newX = remap(node.x, currentX, line[-1].x, element.x, line[-1].x) 110 | node.x = newX 111 | elif element == line[-1]: 112 | newX = remap(node.x, currentX, line[0].x, element.x, line[0].x) 113 | node.x = newX 114 | 115 | 116 | # from @mekkablue snippets 117 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): # don't change x to y for horizontal / vertical DIRECTION 118 | x = thisPoint.x 119 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 120 | italicAngle = radians(italicAngle) # convert to radians 121 | tangens = tan(italicAngle) # math.tan needs radians 122 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 123 | x += horizontalDeviance # x of point that is yOffset from pivotal point 124 | return NSPoint(int(x), thisPoint.y) 125 | 126 | 127 | # ---------------------------------------- 128 | 129 | 130 | layer = Glyphs.font.selectedLayers[0] 131 | selection = layer.selection 132 | 133 | 134 | def getSelectedPaths(): 135 | selectedPaths = [] 136 | for path in layer.paths: 137 | if path.selected: 138 | selectedPaths.append(path) 139 | return selectedPaths 140 | 141 | 142 | selectedPaths = getSelectedPaths() 143 | 144 | 145 | def alignToGuides(): 146 | # collect guides 147 | metricGuides = [0, int(layer.width / 2), layer.width] # check this for italic guides 148 | guides = [] + metricGuides 149 | if len(selection) == 1: 150 | # prev next oncurves as guides 151 | node = selection[0] 152 | nodeGuides = [] 153 | try: 154 | guideNode = None 155 | if node.prevNode.type != 'offcurve': 156 | guideNode = node.prevNode 157 | elif node.prevNode.prevNode.prevNode != 'offcurve': 158 | guideNode = node.prevNode.prevNode.prevNode 159 | if guideNode and guideNode.x not in guides and node != guideNode: 160 | guides.append(guideNode.x) 161 | nodeGuides.append(guideNode) 162 | except: pass 163 | try: 164 | guideNode = None 165 | if node.nextNode.type != 'offcurve': 166 | guideNode = node.nextNode 167 | elif node.nextNode.nextNode.nextNode != 'offcurve': 168 | guideNode = node.nextNode.nextNode.nextNode 169 | if guideNode and guideNode.x not in guides and node != guideNode: 170 | guides.append(guideNode.x) 171 | nodeGuides.append(guideNode) 172 | except: pass 173 | guides.sort() 174 | 175 | # check italic 176 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 177 | if italicAngle != 0: 178 | # italicize guides 179 | italicGuides = [] 180 | for guide in guides: 181 | if guide in metricGuides: 182 | italicGuide = italicize(NSPoint(guide, 0), italicAngle, layer.master.xHeight / 2)[0] 183 | else: 184 | for nodeGuide in nodeGuides: 185 | if guide == nodeGuide.x: 186 | italicGuide = italicize(NSPoint(guide, 0), italicAngle, nodeGuide.y)[0] 187 | italicGuides.append(italicGuide) 188 | italicGuides.sort() 189 | guides = italicGuides 190 | 191 | # < backslant layer 192 | ySkew = tan(radians(italicAngle)) 193 | layer.applyTransform(( 194 | 1.0, # x scale factor 195 | 0.0, # x skew factor 196 | -ySkew, # y skew factor 197 | 1.0, # y scale factor 198 | 0.0, # x position 199 | 0.0 # y position 200 | )) 201 | 202 | # get closest guide 203 | if DIRECTION == 'Left': 204 | currentX = layer.selectionBounds.origin.x 205 | closestGuide = guides[0] 206 | for guide in guides: 207 | if guide < currentX and currentX - guide < currentX - closestGuide: 208 | closestGuide = guide 209 | elif DIRECTION == 'Right': 210 | currentX = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 211 | closestGuide = guides[-1] 212 | for guide in guides: 213 | if guide > currentX and guide - currentX < closestGuide - currentX: 214 | closestGuide = guide 215 | 216 | # align to the guide 217 | shiftX = closestGuide - currentX 218 | for node in selection: 219 | node.x += shiftX 220 | 221 | # set smooth 222 | if len(selection) == 1: 223 | try: 224 | keepSmooth(selection[0], currentX) 225 | except: pass 226 | 227 | # if italic, slant back 228 | if italicAngle != 0: 229 | ySkew = tan(radians(italicAngle)) 230 | layer.applyTransform(( 231 | 1.0, # x scale factor 232 | 0.0, # x skew factor 233 | ySkew, # y skew factor 234 | 1.0, # y scale factor 235 | 0.0, # x position 236 | 0.0 # y position 237 | )) 238 | 239 | 240 | def alignToSelection(): 241 | for element in selection: 242 | # align components 243 | if isinstance(element, GSComponent): 244 | x = int(element.bounds.origin.x - element.x) # Glyphs 2 and 3 have different x y of components 245 | if DIRECTION == 'Left': 246 | element.x = layer.selectionBounds.origin.x - x 247 | elif DIRECTION == 'Right': 248 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width - element.bounds.size.width - x 249 | 250 | # align nodes 251 | elif isinstance(element, GSNode): 252 | align = True 253 | if selectedPaths: 254 | for path in selectedPaths: 255 | if element in path.nodes: 256 | align = False 257 | break 258 | 259 | if align is True: 260 | currentX = element.x 261 | if DIRECTION == 'Left': 262 | element.x = layer.selectionBounds.origin.x 263 | elif DIRECTION == 'Right': 264 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 265 | keepSmooth(element, currentX) 266 | 267 | # align anchors 268 | else: 269 | if DIRECTION == 'Left': 270 | element.x = layer.selectionBounds.origin.x 271 | elif DIRECTION == 'Right': 272 | element.x = layer.selectionBounds.origin.x + layer.selectionBounds.size.width 273 | 274 | # align paths 275 | if selectedPaths: 276 | for path in selectedPaths: 277 | if DIRECTION == 'Left': 278 | # get highest node in the path 279 | leftest = None 280 | for node in path.nodes: 281 | if leftest is None: 282 | leftest = node.x 283 | elif node.x < leftest: 284 | leftest = node.x 285 | shiftX = layer.selectionBounds[0].x - leftest 286 | elif DIRECTION == 'Right': 287 | rightest = None 288 | for node in path.nodes: 289 | if rightest is None: 290 | rightest = node.x 291 | elif node.x > rightest: 292 | rightest = node.x 293 | shiftX = layer.selectionBounds[0].x + layer.selectionBounds[1].width - rightest 294 | 295 | path.applyTransform(( 296 | 1, # x scale factor 297 | 0, # x skew factor 298 | 0, # y skew factor 299 | 1, # y scale factor 300 | shiftX, # x position 301 | 0 # y position 302 | )) 303 | 304 | 305 | # see if all selected points share Y coordinate 306 | sameX = True 307 | for element in selection: 308 | if element.x != selection[0].x: 309 | sameX = False 310 | break 311 | 312 | # in caps lock mode, selection aligns to guides 313 | cpsKeyFlag = 65536 314 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 315 | 316 | # if there’s only one element or path, align it to the guides 317 | # or caps lock is on 318 | if ( 319 | len(selection) == 1 320 | or sameX is True 321 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 322 | or cpsPressed 323 | ): 324 | alignToGuides() 325 | 326 | # if more than one element is selected 327 | else: 328 | alignToSelection() 329 | 330 | # update metrics 331 | layer.updateMetrics() 332 | 333 | ''' 334 | smooth variations 335 | 336 | possible combinations 337 | 338 | OFFCURVE - smooth - offcurve 339 | OFFCURVE - SMOOTH - offcurve 340 | OFFCURVE - smooth - OFFCURVE 341 | offcurve - SMOOTH - offcurve 342 | offcurve - SMOOTH - OFFCURVE 343 | offcurve - smooth - OFFCURVE 344 | 345 | ONCURVE - smooth - offcurve 346 | ONCURVE - SMOOTH - offcurve 347 | ONCURVE - smooth - OFFCURVE 348 | oncurve - SMOOTH - offcurve 349 | oncurve - SMOOTH - OFFCURVE 350 | oncurve - smooth - OFFCURVE 351 | 352 | OFFCURVE - smooth - oncurve 353 | OFFCURVE - SMOOTH - oncurve 354 | OFFCURVE - smooth - ONCURVE 355 | offcurve - SMOOTH - oncurve 356 | offcurve - SMOOTH - ONCURVE 357 | offcurve - smooth - ONCURVE 358 | 359 | OFFCURVE - smooth - smooth - offcurve 360 | OFFCURVE - SMOOTH - smooth - offcurve 361 | OFFCURVE - SMOOTH - SMOOTH - offcurve 362 | offcurve - SMOOTH - SMOOTH - OFFCURVE 363 | offcurve - smooth - SMOOTH - OFFCURVE 364 | offcurve - smooth - smooth - OFFCURVE 365 | 366 | OFFCURVE - smooth - SMOOTH - offcurve 367 | offcurve - SMOOTH - smooth - OFFCURVE 368 | offcurve - smooth - SMOOTH - offcurve 369 | offcurve - SMOOTH - smooth - offcurve 370 | 371 | 372 | 373 | align everything if: 374 | more than 2 selected 375 | OFFCURVE - SMOOTH - offcurve 376 | offcurve - SMOOTH - OFFCURVE 377 | OFFCURVE - smooth - OFFCURVE 378 | ONCURVE - SMOOTH - offcurve 379 | oncurve - SMOOTH - OFFCURVE 380 | ONCURVE - smooth - OFFCURVE 381 | OFFCURVE - SMOOTH - oncurve 382 | offcurve - SMOOTH - ONCURVE 383 | OFFCURVE - smooth - ONCURVE 384 | OFFCURVE - SMOOTH - smooth - offcurve 385 | OFFCURVE - SMOOTH - SMOOTH - offcurve 386 | offcurve - SMOOTH - SMOOTH - OFFCURVE 387 | offcurve - smooth - SMOOTH - OFFCURVE 388 | OFFCURVE - smooth - SMOOTH - offcurve 389 | offcurve - SMOOTH - smooth - OFFCURVE 390 | 391 | keep smooth if: 392 | if line lenght 3 and one offcurve is not selected 393 | OFFCURVE - smooth - offcurve 394 | offcurve - smooth - OFFCURVE 395 | ONCURVE - smooth - offcurve 396 | offcurve - smooth - ONCURVE 397 | 398 | offcurve - SMOOTH - oncurve 399 | oncurve - SMOOTH - offcurve 400 | 401 | recalc Ys if: 402 | if line lenght 3 and 1 oncurve is not selected or line lenght 4 and only 1 offcurve is selected 403 | OFFCURVE - smooth - oncurve 404 | oncurve - smooth - OFFCURVE 405 | OFFCURVE - smooth - smooth - offcurve 406 | offcurve - smooth - smooth - OFFCURVE 407 | 408 | 409 | push everything if: 410 | only one node is selected and it's smooth and the other node is not non smooth offcurve 411 | offcurve - SMOOTH - offcurve 412 | offcurve - smooth - SMOOTH - offcurve 413 | offcurve - SMOOTH - smooth - offcurve 414 | ''' 415 | -------------------------------------------------------------------------------- /Align/Align Top.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Top 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the bottom of the selection or nearest metrics. 7 | """ 8 | 9 | from math import radians, tan 10 | from Foundation import NSPoint, NSEvent 11 | from GlyphsApp import Glyphs, GSComponent, GSNode 12 | 13 | DIRECTION = 'Top' # either 'Top' or 'Bottom' 14 | 15 | 16 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 17 | try: 18 | oldRange = (oldMax - oldMin) 19 | newRange = (newMax - newMin) 20 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 21 | return newValue 22 | except: 23 | return None 24 | 25 | 26 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 27 | # get the smooth line of nodes 28 | line = [] 29 | 30 | if element.smooth: 31 | # 4 nodes line 32 | if prev.smooth or next.smooth: 33 | if prev.smooth: 34 | line = [prevPrev, prev, element, next] 35 | elif next.smooth: 36 | line = [prev, element, next, nextNext] 37 | # 3 nodes line 38 | else: 39 | line = [prev, element, next] 40 | 41 | elif prev.smooth: 42 | if prevPrev.smooth: 43 | line = [prevPrevPrev, prevPrev, prev, element] 44 | else: 45 | line = [prevPrev, prev, element] 46 | elif next.smooth: 47 | if nextNext.smooth: 48 | line = [nextNextNext, nextNext, next, element] 49 | else: 50 | line = [nextNext, next, element] 51 | return line 52 | 53 | 54 | def keepSmooth(element, currentY): 55 | try: 56 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 57 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 58 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 59 | selectedInLine = [] 60 | except: 61 | line = [] 62 | 63 | if line: 64 | for node in line: 65 | if node.selected: 66 | selectedInLine.append(node) 67 | 68 | # align everything if more than 2 nodes in line are selected 69 | if len(selectedInLine) > 1: 70 | # unless they all have the same x 71 | for node in line: 72 | if node.selected or node.x != element.x: 73 | node.y = element.y 74 | 75 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 76 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 77 | pass 78 | # for node in line: 79 | # if node != element: 80 | # node.y += (element.y - currentY) 81 | 82 | # keep smooth if line len == 3 and one offcurve is not selected 83 | elif ( 84 | len(line) == 3 85 | and ( 86 | (line[0].type == 'offcurve' and line[0].selected is False) 87 | or (line[2].type == 'offcurve' and line[2].selected is False) 88 | ) 89 | ): 90 | if (line[0].type == 'offcurve' and line[0].selected is False): 91 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 92 | else: 93 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 94 | 95 | # keep smooth if line len == 4 and only one oncurve is selected 96 | elif ( 97 | len(line) == 4 98 | and len(selectedInLine) == 1 99 | ): 100 | if line[1].selected or line[2].selected: 101 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 102 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 103 | 104 | # otherwise adjust Y 105 | else: 106 | for node in line: 107 | if node != line[0] and node != line[-1]: 108 | if element == line[0]: 109 | 110 | newY = remap(node.y, currentY, line[-1].y, element.y, line[-1].y) 111 | node.y = newY 112 | elif element == line[-1]: 113 | newY = remap(node.y, currentY, line[0].y, element.y, line[0].y) 114 | node.y = newY 115 | 116 | 117 | # from @mekkablue snippets 118 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): 119 | x = thisPoint.x 120 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 121 | italicAngle = radians(italicAngle) # convert to radians 122 | tangens = tan(italicAngle) # math.tan needs radians 123 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 124 | x += horizontalDeviance # x of point that is yOffset from pivotal point 125 | return NSPoint(int(x), thisPoint.y) 126 | 127 | 128 | # ---------------------------------------- 129 | 130 | 131 | layer = Glyphs.font.selectedLayers[0] 132 | selection = layer.selection 133 | 134 | 135 | def getSelectedPaths(): 136 | selectedPaths = [] 137 | for path in layer.paths: 138 | if path.selected: 139 | selectedPaths.append(path) 140 | return selectedPaths 141 | 142 | 143 | selectedPaths = getSelectedPaths() 144 | 145 | 146 | def alignToGuides(): 147 | # get guides and alignment zones 148 | if Glyphs.versionNumber < 3: # Glyphs 2 149 | layerWidth, ascender, capHeight, descender, xHeight, italicAngle, unclear, midXheight = layer.glyphMetrics() 150 | descenderZone = layer.master.alignmentZoneForMetric_(layer.master.descender) 151 | baselineZone = layer.master.alignmentZoneForMetric_(0) 152 | xHeightZone = layer.master.alignmentZoneForMetric_(layer.master.xHeight) 153 | capHeightZone = layer.master.alignmentZoneForMetric_(layer.master.capHeight) 154 | ascenderZone = layer.master.alignmentZoneForMetric_(layer.master.ascender) 155 | # Known bug: special layers get zones from masters. Not sure how to access layer’s zones 156 | else: # Glyphs 3 157 | ascenderZone, capHeightZone, xHeightZone, baselineZone, descenderZone = layer.metrics 158 | ascender, capHeight, xHeight, baseline, descender = ascenderZone.position, capHeightZone.position, xHeightZone.position, baselineZone.position, descenderZone.position 159 | guides = [descender, 0, xHeight, capHeight, ascender, int(xHeight / 2), int((xHeight + capHeight) / 4), int(capHeight / 2)] 160 | 161 | if descenderZone: 162 | guides.append(descender + descenderZone.size) 163 | if baselineZone: 164 | guides.append(baselineZone.size) 165 | if xHeightZone: 166 | guides.append(xHeight + xHeightZone.size) 167 | if capHeightZone: 168 | guides.append(capHeight + capHeightZone.size) 169 | if ascenderZone: 170 | guides.append(ascender + ascenderZone.size) 171 | guides.sort() 172 | 173 | if len(selection) == 1: 174 | # prev next oncurves as guides 175 | node = selection[0] 176 | try: 177 | guideNode = None 178 | if node.prevNode.type != 'offcurve': 179 | guideNode = node.prevNode 180 | elif node.prevNode.prevNode.prevNode != 'offcurve': 181 | guideNode = node.prevNode.prevNode.prevNode 182 | if guideNode and guideNode.y not in guides and node != guideNode: 183 | guides.append(guideNode.y) 184 | except: pass 185 | try: 186 | guideNode = None 187 | if node.nextNode.type != 'offcurve': 188 | guideNode = node.nextNode 189 | elif node.nextNode.nextNode.nextNode != 'offcurve': 190 | guideNode = node.nextNode.nextNode.nextNode 191 | if guideNode and guideNode.y not in guides and node != guideNode: 192 | guides.append(guideNode.y) 193 | except: pass 194 | guides.sort() 195 | 196 | # get closest guide and shiftY 197 | if DIRECTION == 'Bottom': 198 | currentY = layer.selectionBounds.origin.y 199 | closestGuide = guides[0] 200 | elif DIRECTION == 'Top': 201 | currentY = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 202 | closestGuide = guides[-1] 203 | 204 | for guide in guides: 205 | if DIRECTION == 'Bottom': 206 | if guide < currentY and currentY - guide < currentY - closestGuide: 207 | closestGuide = guide 208 | elif DIRECTION == 'Top': 209 | if guide > currentY and guide - currentY < closestGuide - currentY: 210 | closestGuide = guide 211 | 212 | shiftY = closestGuide - currentY 213 | # align 214 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 215 | for node in selection: 216 | node.y += shiftY 217 | if italicAngle != 0: 218 | node.x = italicize(node, italicAngle, node.y - shiftY).x 219 | # keep smooth 220 | if len(selection) == 1: 221 | try: 222 | keepSmooth(selection[0], currentY) 223 | except: pass 224 | 225 | 226 | def alignToSelection(): 227 | for element in selection: 228 | # align components 229 | if isinstance(element, GSComponent): 230 | y = int(element.bounds.origin.y - element.y) # Glyphs 2 and 3 have different x y of components 231 | if DIRECTION == 'Bottom': 232 | element.y = layer.selectionBounds.origin.y - y 233 | elif DIRECTION == 'Top': 234 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height - element.bounds.size.height - y 235 | 236 | # align nodes 237 | elif isinstance(element, GSNode): 238 | align = True 239 | if selectedPaths: 240 | for path in selectedPaths: 241 | if element in path.nodes: 242 | align = False 243 | break 244 | 245 | if align is True: 246 | currentY = element.y 247 | if DIRECTION == 'Bottom': 248 | element.y = layer.selectionBounds.origin.y 249 | elif DIRECTION == 'Top': 250 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 251 | keepSmooth(element, currentY) 252 | 253 | # align anchors 254 | else: 255 | if DIRECTION == 'Bottom': 256 | element.y = layer.selectionBounds.origin.y 257 | elif DIRECTION == 'Top': 258 | element.y = layer.selectionBounds.origin.y + layer.selectionBounds.size.height 259 | # align paths 260 | if selectedPaths: 261 | for path in selectedPaths: 262 | if DIRECTION == 'Bottom': 263 | # get highest node in the path 264 | lowest = None 265 | for node in path.nodes: 266 | if lowest is None: 267 | lowest = node.y 268 | elif node.y < lowest: 269 | lowest = node.y 270 | shift = layer.selectionBounds.origin.y - lowest 271 | elif DIRECTION == 'Top': 272 | highest = None 273 | for node in path.nodes: 274 | if highest is None: 275 | highest = node.y 276 | elif node.y > highest: 277 | highest = node.y 278 | shift = layer.selectionBounds.origin.y + layer.selectionBounds.size.height - highest 279 | 280 | path.applyTransform(( 281 | 1, # x scale factor 282 | 0, # x skew factor 283 | 0, # y skew factor 284 | 1, # y scale factor 285 | 0, # x position 286 | shift # y position 287 | )) 288 | 289 | 290 | # see if all selected points share Y coordinate 291 | sameY = True 292 | for element in selection: 293 | if element.y != selection[0].y: 294 | sameY = False 295 | break 296 | 297 | # in caps lock mode, selection aligns to guides 298 | cpsKeyFlag = 65536 299 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 300 | 301 | # if there’s only one element or path, align it to the guides 302 | # or caps lock is on 303 | if ( 304 | len(selection) == 1 305 | or sameY is True 306 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 307 | or cpsPressed 308 | ): 309 | alignToGuides() 310 | 311 | # if more than one element is selected 312 | else: 313 | alignToSelection() 314 | 315 | # update metrics 316 | layer.updateMetrics() 317 | 318 | 319 | 320 | ''' 321 | smooth variations 322 | 323 | possible combinations 324 | 325 | OFFCURVE - smooth - offcurve 326 | OFFCURVE - SMOOTH - offcurve 327 | OFFCURVE - smooth - OFFCURVE 328 | offcurve - SMOOTH - offcurve 329 | offcurve - SMOOTH - OFFCURVE 330 | offcurve - smooth - OFFCURVE 331 | 332 | ONCURVE - smooth - offcurve 333 | ONCURVE - SMOOTH - offcurve 334 | ONCURVE - smooth - OFFCURVE 335 | oncurve - SMOOTH - offcurve 336 | oncurve - SMOOTH - OFFCURVE 337 | oncurve - smooth - OFFCURVE 338 | 339 | OFFCURVE - smooth - oncurve 340 | OFFCURVE - SMOOTH - oncurve 341 | OFFCURVE - smooth - ONCURVE 342 | offcurve - SMOOTH - oncurve 343 | offcurve - SMOOTH - ONCURVE 344 | offcurve - smooth - ONCURVE 345 | 346 | OFFCURVE - smooth - smooth - offcurve 347 | OFFCURVE - SMOOTH - smooth - offcurve 348 | OFFCURVE - SMOOTH - SMOOTH - offcurve 349 | offcurve - SMOOTH - SMOOTH - OFFCURVE 350 | offcurve - smooth - SMOOTH - OFFCURVE 351 | offcurve - smooth - smooth - OFFCURVE 352 | 353 | OFFCURVE - smooth - SMOOTH - offcurve 354 | offcurve - SMOOTH - smooth - OFFCURVE 355 | offcurve - smooth - SMOOTH - offcurve 356 | offcurve - SMOOTH - smooth - offcurve 357 | 358 | 359 | 360 | align everything if: 361 | more than 2 selected 362 | OFFCURVE - SMOOTH - offcurve 363 | offcurve - SMOOTH - OFFCURVE 364 | OFFCURVE - smooth - OFFCURVE 365 | ONCURVE - SMOOTH - offcurve 366 | oncurve - SMOOTH - OFFCURVE 367 | ONCURVE - smooth - OFFCURVE 368 | OFFCURVE - SMOOTH - oncurve 369 | offcurve - SMOOTH - ONCURVE 370 | OFFCURVE - smooth - ONCURVE 371 | OFFCURVE - SMOOTH - smooth - offcurve 372 | OFFCURVE - SMOOTH - SMOOTH - offcurve 373 | offcurve - SMOOTH - SMOOTH - OFFCURVE 374 | offcurve - smooth - SMOOTH - OFFCURVE 375 | OFFCURVE - smooth - SMOOTH - offcurve 376 | offcurve - SMOOTH - smooth - OFFCURVE 377 | 378 | keep smooth if: 379 | if line lenght 3 and one offcurve is not selected 380 | OFFCURVE - smooth - offcurve 381 | offcurve - smooth - OFFCURVE 382 | ONCURVE - smooth - offcurve 383 | offcurve - smooth - ONCURVE 384 | 385 | offcurve - SMOOTH - oncurve 386 | oncurve - SMOOTH - offcurve 387 | 388 | recalc Ys if: 389 | if line lenght 3 and 1 oncurve is not selected or line lenght 4 and only 1 offcurve is selected 390 | OFFCURVE - smooth - oncurve 391 | oncurve - smooth - OFFCURVE 392 | OFFCURVE - smooth - smooth - offcurve 393 | offcurve - smooth - smooth - OFFCURVE 394 | 395 | 396 | push everything if: 397 | only one node is selected and it's smooth and the other node is not non smooth offcurve 398 | offcurve - SMOOTH - offcurve 399 | offcurve - smooth - SMOOTH - offcurve 400 | offcurve - SMOOTH - smooth - offcurve 401 | ''' 402 | -------------------------------------------------------------------------------- /Align/Align Vertical Center.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Align Vertical Center 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Aligns nodes and components to the vertical center of the selection or nearest metrics. 7 | """ 8 | 9 | from math import radians, tan 10 | from Foundation import NSPoint, NSEvent 11 | from GlyphsApp import Glyphs, GSComponent, GSNode 12 | 13 | 14 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 15 | try: 16 | oldRange = (oldMax - oldMin) 17 | newRange = (newMax - newMin) 18 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 19 | return newValue 20 | except: 21 | return None 22 | 23 | 24 | def getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext): 25 | # get the smooth line of nodes 26 | line = [] 27 | 28 | if element.smooth: 29 | # 4 nodes line 30 | if prev.smooth or next.smooth: 31 | if prev.smooth: 32 | line = [prevPrev, prev, element, next] 33 | elif next.smooth: 34 | line = [prev, element, next, nextNext] 35 | # 3 nodes line 36 | else: 37 | line = [prev, element, next] 38 | 39 | elif prev.smooth: 40 | if prevPrev.smooth: 41 | line = [prevPrevPrev, prevPrev, prev, element] 42 | else: 43 | line = [prevPrev, prev, element] 44 | elif next.smooth: 45 | if nextNext.smooth: 46 | line = [nextNextNext, nextNext, next, element] 47 | else: 48 | line = [nextNext, next, element] 49 | return line 50 | 51 | 52 | def keepSmooth(element, currentY): 53 | try: 54 | prev, prevPrev, prevPrevPrev = element.prevNode, element.prevNode.prevNode, element.prevNode.prevNode.prevNode 55 | next, nextNext, nextNextNext = element.nextNode, element.nextNode.nextNode, element.nextNode.nextNode.nextNode 56 | line = getSmoothLine(element, prev, prevPrev, prevPrevPrev, next, nextNext, nextNextNext) 57 | selectedInLine = [] 58 | except: 59 | line = [] 60 | 61 | if line: 62 | for node in line: 63 | if node.selected: 64 | selectedInLine.append(node) 65 | 66 | # align everything if more than 2 nodes in line are selected 67 | if len(selectedInLine) > 1: 68 | # unless they all have the same x 69 | for node in line: 70 | if node.selected or node.x != element.x: 71 | node.y = element.y 72 | 73 | # shift everything if only one node is selected and it's smooth and the other nodes are offcurves 74 | elif element.smooth and prev.type == 'offcurve' and next.type == 'offcurve': 75 | pass 76 | # for node in line: 77 | # if node != element: 78 | # node.y += (element.y - currentY) 79 | 80 | # keep smooth if line len == 3 and one offcurve is not selected 81 | elif ( 82 | len(line) == 3 83 | and ( 84 | (line[0].type == 'offcurve' and line[0].selected is False) 85 | or (line[2].type == 'offcurve' and line[2].selected is False) 86 | ) 87 | ): 88 | if (line[0].type == 'offcurve' and line[0].selected is False): 89 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 90 | else: 91 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[2], line[1], line[0]) 92 | 93 | # keep smooth if line len == 4 and only one oncurve is selected 94 | elif ( 95 | len(line) == 4 96 | and len(selectedInLine) == 1 97 | ): 98 | if line[1].selected or line[2].selected: 99 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[0], line[1], line[2]) 100 | element.parent.setSmooth_withCenterNode_oppositeNode_(line[3], line[2], line[1]) 101 | 102 | # otherwise adjust Y 103 | else: 104 | for node in line: 105 | if node != line[0] and node != line[-1]: 106 | if element == line[0]: 107 | newY = remap(node.y, currentY, line[-1].y, element.y, line[-1].y) # doesn't work if y difference was 0 108 | node.y = newY 109 | elif element == line[-1]: 110 | newY = remap(node.y, currentY, line[0].y, element.y, line[0].y) 111 | node.y = newY 112 | 113 | 114 | # from @mekkablue snippets 115 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): 116 | x = thisPoint.x 117 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 118 | italicAngle = radians(italicAngle) # convert to radians 119 | tangens = tan(italicAngle) # math.tan needs radians 120 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 121 | x += horizontalDeviance # x of point that is yOffset from pivotal point 122 | return NSPoint(int(x), thisPoint.y) 123 | 124 | 125 | # ---------------------------------------- 126 | 127 | 128 | layer = Glyphs.font.selectedLayers[0] 129 | selection = layer.selection 130 | 131 | 132 | def getSelectedPaths(): 133 | selectedPaths = [] 134 | for path in layer.paths: 135 | if path.selected: 136 | selectedPaths.append(path) 137 | return selectedPaths 138 | 139 | 140 | selectedPaths = getSelectedPaths() 141 | 142 | 143 | def alignToGuides(): 144 | # get guides and alignment zones 145 | if Glyphs.versionNumber < 3: # Glyphs 2 146 | layerWidth, ascender, capHeight, descender, xHeight, italicAngle, unclear, midXheight = layer.glyphMetrics() 147 | else: # Glyphs 3 148 | ascenderZone, capHeightZone, xHeightZone, baselineZone, descenderZone = layer.metrics 149 | xHeight, capHeight = xHeightZone.position, capHeightZone.position 150 | 151 | # collect guides (mid xHeight, mid xHeight and capHeight, mid cap height) 152 | guides = [int(xHeight / 2), int((xHeight + capHeight) / 4), int(capHeight / 2)] 153 | guides.sort() 154 | # get closest guide and shiftY 155 | currentY = layer.selectionBounds.origin.y + (layer.selectionBounds.size.height / 2) 156 | for i, guide in enumerate(guides): 157 | if currentY < guide: 158 | closestGuide = guide 159 | break 160 | elif currentY >= guides[-1]: 161 | closestGuide = guides[0] 162 | shiftY = closestGuide - currentY 163 | # align 164 | italicAngle = layer.italicAngle() if Glyphs.versionNumber < 3 else layer.italicAngle 165 | for node in selection: 166 | node.y += shiftY 167 | if italicAngle != 0: 168 | node.x = italicize(node, italicAngle, node.y - shiftY).x 169 | # keep smooth 170 | if len(selection) == 1: 171 | try: 172 | keepSmooth(selection[0], currentY) 173 | except: pass 174 | 175 | 176 | def alignToSelectionCenter(): 177 | selectionCenter = layer.selectionBounds.origin.y + layer.selectionBounds.size.height / 2 178 | for element in selection: 179 | # align components 180 | if isinstance(element, GSComponent): 181 | y = int(element.bounds.origin.y - element.y) # Glyphs 2 and 3 have different x y of components 182 | element.y = selectionCenter - element.bounds.size.height / 2 - y 183 | 184 | # align nodes 185 | elif isinstance(element, GSNode): 186 | align = True 187 | if selectedPaths: 188 | for path in selectedPaths: 189 | if element in path.nodes: 190 | align = False 191 | break 192 | 193 | if align is True: 194 | currentY = element.y 195 | element.y = selectionCenter 196 | keepSmooth(element, currentY) 197 | 198 | # align anchors 199 | else: 200 | element.y = selectionCenter 201 | 202 | # align paths 203 | if selectedPaths: 204 | for path in selectedPaths: 205 | pathCenter = path.bounds.origin.y + path.bounds.size.height / 2 206 | shiftY = selectionCenter - pathCenter 207 | 208 | path.applyTransform(( 209 | 1, # x scale factor 210 | 0, # x skew factor 211 | 0, # y skew factor 212 | 1, # y scale factor 213 | 0, # x position 214 | shiftY # y position 215 | )) 216 | 217 | 218 | # see if all selected points share Y coordinate 219 | sameY = True 220 | for element in selection: 221 | if element.y != selection[0].y: 222 | sameY = False 223 | break 224 | 225 | # in caps lock mode, selection aligns to guides 226 | cpsKeyFlag = 65536 227 | cpsPressed = NSEvent.modifierFlags() & cpsKeyFlag == cpsKeyFlag 228 | 229 | # if there’s only one element or path, align it to the guides 230 | # or caps lock is on 231 | if ( 232 | len(selection) == 1 233 | or sameY is True 234 | or (len(selectedPaths) == 1 and len(selection) == len(selectedPaths[0].nodes)) 235 | or cpsPressed): 236 | alignToGuides() 237 | 238 | # if more than one element is selected 239 | else: 240 | alignToSelectionCenter() 241 | 242 | # update metrics 243 | layer.updateMetrics() 244 | -------------------------------------------------------------------------------- /Align/Center Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Center Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Makes equal left and right sidebearings for all layers in the selected glyphs. 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | # Center selected glyphs 12 | 13 | font = Glyphs.font 14 | 15 | if font and font.selectedLayers: 16 | for selectedLayer in font.selectedLayers: 17 | glyph = selectedLayer.parent 18 | for layer in glyph.layers: 19 | width = layer.width 20 | sidebearings = layer.LSB + layer.RSB 21 | layer.LSB = int(sidebearings / 2) 22 | layer.width = width 23 | -------------------------------------------------------------------------------- /Align/Distribute Nodes Horizontally.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Distribute Nodes Horizontally 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Distributes the selected nodes horizontally. 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | # This is a slightly modified vertion of Distribute Nodes script by @mekkablue 12 | 13 | 14 | def distributeNodes(direction): # 0 horizontal, 1 vertical 15 | font = Glyphs.font 16 | if not font.selectedLayers: 17 | return 18 | selectedLayer = font.selectedLayers[0] 19 | try: 20 | try: 21 | # until v2.1: 22 | selection = selectedLayer.selection() 23 | except: 24 | # since v2.2: 25 | selection = selectedLayer.selection 26 | # nothing to distribute 27 | if len(selection) < 3: 28 | return 29 | 30 | selectionXList = [n.x for n in selection] 31 | selectionYList = [n.y for n in selection] 32 | leftMostX, rightMostX = min(selectionXList), max(selectionXList) 33 | lowestY, highestY = min(selectionYList), max(selectionYList) 34 | diffX = abs(leftMostX - rightMostX) 35 | diffY = abs(lowestY - highestY) 36 | 37 | font.disableUpdateInterface() 38 | try: 39 | if direction == 0: # distribute horizontally 40 | increment = diffX / float(len(selection) - 1) 41 | sortedSelection = sorted(selection, key=lambda n: n.x) 42 | for thisNodeIndex in range(len(selection) - 1): 43 | sortedSelection[thisNodeIndex].x = leftMostX + (thisNodeIndex * increment) 44 | else: # distribute vertically 45 | increment = diffY / float(len(selection) - 1) 46 | sortedSelection = sorted(selection, key=lambda n: n.y) 47 | for thisNodeIndex in range(len(selection) - 1): 48 | sortedSelection[thisNodeIndex].y = lowestY + (thisNodeIndex * increment) 49 | except Exception as e: 50 | Glyphs.showMacroWindow() 51 | print("\n⚠️ Script Error:\n") 52 | import traceback 53 | print(traceback.format_exc()) 54 | print() 55 | raise e 56 | finally: 57 | font.enableUpdateInterface() # re-enables UI updates in font View 58 | 59 | except Exception as e: 60 | if selection == (): 61 | print("Cannot distribute nodes: nothing selected in frontmost layer.") 62 | else: 63 | print("Error. Cannot distribute nodes:", selection) 64 | print(e) 65 | 66 | 67 | distributeNodes(direction=0) 68 | -------------------------------------------------------------------------------- /Align/Distribute Nodes Vertically.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Distribute Nodes Vertically 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | __doc__ = """ 6 | Distributes the selected nodes vertically. 7 | """ 8 | 9 | # This is a slightly modified vertion of Distribute Nodes script by @mekkablue 10 | 11 | from GlyphsApp import Glyphs 12 | 13 | 14 | def distributeNodes(direction): # 0 horizontal, 1 vertical 15 | font = Glyphs.font 16 | if not font.selectedLayers: 17 | return 18 | selectedLayer = font.selectedLayers[0] 19 | try: 20 | try: 21 | # until v2.1: 22 | selection = selectedLayer.selection() 23 | except: 24 | # since v2.2: 25 | selection = selectedLayer.selection 26 | # nothing to distribute 27 | if len(selection) < 3: 28 | return 29 | 30 | selectionXList = [n.x for n in selection] 31 | selectionYList = [n.y for n in selection] 32 | leftMostX, rightMostX = min(selectionXList), max(selectionXList) 33 | lowestY, highestY = min(selectionYList), max(selectionYList) 34 | diffX = abs(leftMostX - rightMostX) 35 | diffY = abs(lowestY - highestY) 36 | 37 | font.disableUpdateInterface() 38 | try: 39 | if direction == 0: # distribute horizontally 40 | increment = diffX / float(len(selection) - 1) 41 | sortedSelection = sorted(selection, key=lambda n: n.x) 42 | for thisNodeIndex in range(len(selection) - 1): 43 | sortedSelection[thisNodeIndex].x = leftMostX + (thisNodeIndex * increment) 44 | else: # distribute vertically 45 | increment = diffY / float(len(selection) - 1) 46 | sortedSelection = sorted(selection, key=lambda n: n.y) 47 | for thisNodeIndex in range(len(selection) - 1): 48 | sortedSelection[thisNodeIndex].y = lowestY + (thisNodeIndex * increment) 49 | except Exception as e: 50 | Glyphs.showMacroWindow() 51 | print("\n⚠️ Script Error:\n") 52 | import traceback 53 | print(traceback.format_exc()) 54 | print() 55 | raise e 56 | finally: 57 | font.enableUpdateInterface() # re-enables UI updates in font View 58 | 59 | except Exception as e: 60 | if selection == (): 61 | print("Cannot distribute nodes: nothing selected in frontmost layer.") 62 | else: 63 | print("Error. Cannot distribute nodes:", selection) 64 | print(e) 65 | 66 | 67 | distributeNodes(direction=1) 68 | -------------------------------------------------------------------------------- /App/DarkMode.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Dark Mode 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggles dark mode preview. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | if Glyphs.versionNumber >= 3: 11 | if Glyphs.defaults["GSEditViewDarkMode"] == 1: 12 | Glyphs.defaults["GSEditViewDarkMode"] = 0 13 | else: 14 | Glyphs.defaults["GSEditViewDarkMode"] = 1 15 | else: 16 | if Glyphs.defaults["GSEditViewDarkMode"] is True: 17 | Glyphs.defaults["GSEditViewDarkMode"] = False 18 | else: 19 | Glyphs.defaults["GSEditViewDarkMode"] = True 20 | -------------------------------------------------------------------------------- /App/Export To All Formats.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Export To All Formats 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Export to all formats at once. 6 | WEB formats are WOFF and WOFF2. If “PS outlines” is off, TT outlines will be exported for the web formats. 7 | Compress OTF/TTF option converts exported OTF/TTF to web formats, which is faster than exporting them from Glyphs. 8 | Supports Static Settings, which is a way to bulk-apply custom parameters or properties, similar to variable instances. Just add deactivated instances with 'Static Setting' in their names and custom parameters. 9 | """ 10 | 11 | Glyphs3 = Glyphs.versionNumber >= 3 12 | 13 | import os, sys 14 | from GlyphsApp import Glyphs, GetFolder, OTF, TTF, VARIABLE, PLAIN, WOFF, WOFF2 15 | if Glyphs3: 16 | from GlyphsApp import INSTANCETYPEVARIABLE 17 | from vanilla import FloatingWindow, TextBox, CheckBox, SquareButton, Button, ProgressBar 18 | try: 19 | from fontTools import ttLib 20 | fontToolsImported = True 21 | except: 22 | fontToolsImported = False 23 | 24 | Glyphs.clearLog() 25 | 26 | # NOTES: 27 | # STATIC SETTING 28 | # Will apply instance properties and custom parameters from instances called 'Static Setting' (each will be exported separately). 29 | # Replace family names: familyName = 'Right Grotesk -> Right Sans'. Replacement is useful when instance familyNames are like Right Grotesk Text, Right Grotesk Display, etc. 30 | # For variable exports, ensures that origin’s style name = variable style name (Glyphs 3 bug) 31 | 32 | # STAT EXPORT 33 | # post-processes exported variable files if (basically runs a copy of @mekkablue’s Read and Write STAT Axis Values (OTVAR) after exporting variable instances). Hopefully this is temp 34 | 35 | 36 | # window size and margin 37 | M = 15 38 | W, H = 380, M * 21 39 | captionWidth = 130 40 | columnWidth = 45 41 | lineYs = [M, M * 3, M * 5, M * 7, M * 9, M * 11, M * 13, M * 15, M * 17, M * 19] # y of each line (tittle, export, outline flavor, remove overlaps, autohint) 42 | lineYs = [y - 7 for y in lineYs] # shift all vertically a bit 43 | 44 | captions = ['', 'Export', 'Autohint', 'Remove Overlaps', 'PS Outlines', 'Compress otf/ttf (faster)'] 45 | staticFormats, variableFormats = ['OTF', 'TTF', 'WEB'], ['Variable', 'VariableWEB'] 46 | formats = staticFormats + variableFormats 47 | 48 | # No Variable WEB in Glyphs 2 49 | if Glyphs3 is False: 50 | formats.remove('VariableWEB') 51 | W -= columnWidth 52 | 53 | alphaActive = 1 54 | alphaDeactivated = .4 55 | 56 | 57 | class CheckBoxWithAlpha(CheckBox): 58 | def setAlpha(self, value=1): 59 | self.getNSButton().setAlphaValue_(value) 60 | def getAlpha(self): 61 | return self.getNSButton().alphaValue 62 | # def getValueCheckAlpha(self): 63 | # return True if self.get() and self.getNSButton().alphaValue == alphaActive else False 64 | 65 | class ExportToAllFormats(): 66 | def __init__(self): 67 | self.exportPath = Glyphs.defaults['OTFExportPath'] 68 | self.postProcessWEB = False 69 | self.postProcessVariableWEB = False 70 | 71 | # window 72 | self.w = FloatingWindow((W, H), 'Export to all formats') 73 | 74 | # make captions for each row 75 | for i in range(len(lineYs))[:6]: 76 | captionID = captions[i] 77 | caption = TextBox((M, lineYs[i], captionWidth*2, M), captions[i]) 78 | setattr(self.w, captionID, caption) 79 | 80 | # make a column of options for each format 81 | for i in range(len(formats)): 82 | x = captionWidth + columnWidth * i 83 | if i > 2: 84 | x += 10 # add a gap to separate variable formats 85 | 86 | # format title 87 | formatTitle = formats[i] if i != 4 else 'WEB' # shorten 'VariableWEB' to another 'WEB' 88 | y = lineYs[0] 89 | titleID = 'title' + formats[i] 90 | title = TextBox((x - columnWidth / 2, y, columnWidth * 2, M), formatTitle, alignment = 'center') 91 | setattr(self.w, titleID, title) 92 | 93 | x += columnWidth / 2 - 7 94 | # export checkbox 95 | y = lineYs[1] 96 | exportCheckBoxID = 'exportCheckBox' + formats[i] 97 | exportCheckBox = CheckBoxWithAlpha((x, y, columnWidth / 2, M), None, value = True, callback = self.checkBoxCallback) 98 | setattr(self.w, exportCheckBoxID, exportCheckBox) 99 | setattr(getattr(self.w, exportCheckBoxID), 'i', i) 100 | 101 | # autohint checkbox 102 | y = lineYs[2] 103 | autohintID = 'autohint' + formats[i] 104 | autohint = CheckBoxWithAlpha((x, y, columnWidth / 2, M), '', value = True) 105 | setattr(self.w, autohintID, autohint) 106 | 107 | # overlaps checkbox (except variable) 108 | if i < 3: 109 | y = lineYs[3] 110 | overlapsID = 'overlaps' + formats[i] 111 | overlaps = CheckBoxWithAlpha((x, y, columnWidth / 2, M), '', value = True) 112 | setattr(self.w, overlapsID, overlaps) 113 | 114 | # PS outlines (web only) 115 | if i == 2: 116 | y = lineYs[4] 117 | outlinesID = 'outlines' + formats[i] 118 | outlines = CheckBoxWithAlpha((x, y, columnWidth / 2, M), '') 119 | setattr(self.w, outlinesID, outlines) 120 | 121 | # post-process WEB 122 | if i in [2, 4]: 123 | y = lineYs[5] 124 | postProcessID = 'postProcess' + formats[i] 125 | postProcess = CheckBoxWithAlpha((x, y, columnWidth / 2, M), '', value = True, callback = self.checkBoxCallback) 126 | postProcess.ID = formats[i] 127 | setattr(self.w, postProcessID, postProcess) 128 | # disable checkbox if fontTools import failed 129 | if fontToolsImported is False: 130 | postProcess.enable(0) 131 | postProcess.setToolTip('FontTools library is missing. Install it in Plugin Manager → Modules') 132 | 133 | 134 | # Export Path 135 | self.w.exportPath = SquareButton((M + 3, lineYs[6], -M - 3, M), 'Export Path', callback = self.exportPathCallback) 136 | if self.exportPath: 137 | self.w.exportPath.setTitle(self.exportPath) 138 | 139 | # Format Subfolders 140 | self.w.subfolders = CheckBox((M+2, lineYs[7], W / 2, M), 'Format folders', value = True) 141 | 142 | # Family Subfolders 143 | self.w.familySubfolders = CheckBox((145, lineYs[7], W / 2, M), 'Family folders', value = True) 144 | 145 | # Unnest components 146 | self.w.unnestComponents = CheckBox((260, lineYs[7], W / 2, M), 'Unnest comps', value = True) 147 | 148 | # Export all open fonts 149 | self.w.exportAll = CheckBox((M+2, lineYs[8], W / 2, M), 'All open fonts') 150 | 151 | # Run button 152 | self.w.run = Button((145, lineYs[8], -M, M), 'Export', callback = self.run) 153 | if self.exportPath: 154 | self.w.exportPath.setTitle(self.exportPath) 155 | 156 | # progress bar 157 | self.w.progress = ProgressBar((M, H + 100, -M, M)) 158 | 159 | # exporting instance 160 | self.w.info = TextBox((M, lineYs[9], -M, M), '') 161 | 162 | # Uncheck variable checkboxes by default 163 | for ID in ['exportCheckBoxVariable', 'exportCheckBoxVariableWEB']: 164 | checkbox = getattr(self.w, ID, None) 165 | if checkbox: 166 | checkbox.toggle() 167 | # self.checkBoxCallback(checkbox) 168 | # # toggle post-process checkbox 169 | self.checkBoxCallback(getattr(self.w, 'postProcessWEB')) 170 | if Glyphs3: 171 | self.checkBoxCallback(getattr(self.w, 'postProcessVariableWEB')) 172 | 173 | self.w.open() 174 | 175 | 176 | def checkBoxCallback(self, sender): 177 | # get ID (postProcess checkboxes’ ID is either 'WEB' or 'VariableWEB') 178 | ID = getattr(sender, 'ID', None) 179 | if ID: 180 | value = sender.get() 181 | if ID == 'VariableWEB': 182 | self.postProcessVariableWEB = value 183 | if self.w.exportCheckBoxVariableWEB.get(): 184 | self.w.autohintVariableWEB.setAlpha(alphaDeactivated if value else alphaActive) 185 | else: # 'WEB' 186 | self.postProcessWEB = value 187 | if self.w.exportCheckBoxWEB.get(): 188 | self.w.autohintWEB.setAlpha(alphaDeactivated if value else alphaActive) 189 | self.w.overlapsWEB.setAlpha(alphaDeactivated if value else alphaActive) 190 | 191 | countFormats = 0 192 | for formt in formats: 193 | alpha = alphaActive if getattr(self.w, 'exportCheckBox' + formt).get() else alphaDeactivated 194 | # add active formats to the counter 195 | if alpha == alphaActive: 196 | countFormats += 1 197 | 198 | if 'WEB' not in formt or (formt == 'VariableWEB' and not self.postProcessVariableWEB) or (formt == 'WEB' and not self.postProcessWEB): 199 | getattr(self.w, 'autohint' + formt).setAlpha(alpha) 200 | if 'Variable' not in formt and (formt != 'WEB' or not self.postProcessWEB): 201 | getattr(self.w, 'overlaps' + formt).setAlpha(alpha) 202 | if formt == 'WEB': 203 | getattr(self.w, 'outlines' + formt).setAlpha(alpha) 204 | if 'WEB' in formt: 205 | getattr(self.w, 'postProcess' + formt).setAlpha(alpha) 206 | 207 | # if WEB set to post-process mode, you can only choose outlines if both OTF/TTF set to export, otherwise it will compress the 1 available 208 | exportOTF = self.w.exportCheckBoxOTF.get() 209 | exportTTF = self.w.exportCheckBoxTTF.get() 210 | exportWEB = self.w.exportCheckBoxWEB.get() 211 | # WEB outlines 212 | alpha = alphaDeactivated 213 | if exportWEB and (exportOTF and exportTTF) or not self.postProcessWEB: 214 | alpha = alphaActive 215 | 216 | getattr(self.w, 'outlinesWEB').setAlpha(alpha) 217 | 218 | 219 | # deactivate run button if no formats are chosen for export 220 | self.w.run.enable(countFormats > 0) 221 | 222 | 223 | def exportPathCallback(self, sender): 224 | newExportPath = GetFolder(message='Export to', allowsMultipleSelection=False) 225 | if newExportPath: 226 | self.exportPath = newExportPath 227 | self.w.exportPath.setTitle(self.exportPath) 228 | self.w.info.set('') 229 | 230 | 231 | def getWebOutlines(self): 232 | exportOTF, exportTTF, compressWEB, PSOutlines = self.w.exportCheckBoxOTF.get(), self.w.exportCheckBoxTTF.get(), self.w.postProcessWEB.get(), self.w.outlinesWEB.get() 233 | if compressWEB: 234 | if (exportOTF and exportTTF) or (not exportOTF and not exportTTF): 235 | return OTF if PSOutlines else TTF 236 | else: 237 | return OTF if exportOTF else TTF 238 | else: 239 | return OTF if PSOutlines else TTF 240 | 241 | 242 | def getFamilyNamesFromStaticSettings(self, font): 243 | staticSettings, exportDefault = self.getStaticSettings(font) 244 | familyNamesFromStaticSettings = set() 245 | for staticSetting in staticSettings: 246 | familyNamesFromStaticSettings.add(self.getFamilyNameForInstance(font.instances[staticSetting])) 247 | return familyNamesFromStaticSettings, exportDefault 248 | 249 | 250 | def applyStaticSettingToFamilyName(self, familyName, familyNamesFromStaticSettings, exportDefault): 251 | familyNames = set([familyName]) if exportDefault else set() 252 | for familyNameFromStaticSettings in familyNamesFromStaticSettings: 253 | if '->' in familyNameFromStaticSettings: 254 | old, new = familyNameFromStaticSettings.split('->') 255 | familyNames.add(familyName.replace(old, new)) 256 | else: 257 | familyNames.add(familyNameFromStaticSettings) 258 | return familyNames 259 | 260 | 261 | def getFontFamilyNames(self, font, selectedFormats): 262 | familyNamesFromStaticSettings, exportDefault = self.getFamilyNamesFromStaticSettings(font) 263 | exportVariable = 'Variable' in selectedFormats or 'VariableWEB' in selectedFormats 264 | exportStatic = 'OTF' in selectedFormats or 'TTF' in selectedFormats or 'WEB' in selectedFormats 265 | 266 | fontFamilyNames = {} 267 | for instance in font.instances: 268 | if instance.active: 269 | familyName = self.getFamilyNameForInstance(instance) 270 | 271 | # variable instance - add familyName 272 | if instance.type == INSTANCETYPEVARIABLE: 273 | if exportVariable: 274 | if familyName not in fontFamilyNames: 275 | fontFamilyNames[familyName] = set() 276 | fontFamilyNames[familyName].add('Variable') 277 | 278 | # static instance - add familyNames for all 'Static Setting's 279 | else: 280 | if exportStatic: 281 | for famName in self.applyStaticSettingToFamilyName(familyName, familyNamesFromStaticSettings, exportDefault): 282 | if famName not in fontFamilyNames: 283 | fontFamilyNames[famName] = set() 284 | fontFamilyNames[famName].add('Static') 285 | 286 | return fontFamilyNames 287 | 288 | 289 | def getAllFamilyNames(self, fonts, selectedFormats): 290 | familyNames = {} 291 | for font in fonts: 292 | fontFamilyNames = self.getFontFamilyNames(font, selectedFormats) 293 | 294 | # merge with the full dict 295 | for familyName, frmts in fontFamilyNames.items(): 296 | if familyName not in familyNames: 297 | familyNames[familyName] = set() 298 | for frmt in frmts: 299 | familyNames[familyName].add(frmt) 300 | return familyNames 301 | 302 | 303 | def createFolders(self, familyNames, selectedFormats): 304 | folders = {} 305 | for familyName, familyNameFormats in familyNames.items(): 306 | 307 | # familyName path 308 | if self.w.familySubfolders.get(): 309 | familyNamePath = self.exportPath + '/' + familyName if len(familyNames) > 1 else self.exportPath 310 | else: 311 | familyNamePath = self.exportPath 312 | 313 | # format path 314 | for formt in selectedFormats: 315 | # format subfolders 316 | if self.w.subfolders.get(): 317 | # skip formats which are not used for the family name 318 | if formt in staticFormats and 'Static' not in familyNameFormats: 319 | continue 320 | if formt in variableFormats and 'Variable' not in familyNameFormats: 321 | continue 322 | 323 | if formt != 'VariableWEB': 324 | formatPath = familyNamePath + '/' + formt + '/' 325 | else: # Put VariableWEB into the variable folder 326 | formatPath = familyNamePath + '/Variable/' 327 | 328 | # create the format folder if missing 329 | if not os.path.exists(formatPath): 330 | os.makedirs(formatPath) 331 | 332 | # add path to dict 333 | if familyName not in folders: 334 | folders[familyName] = {} 335 | folders[familyName][formt] = formatPath 336 | 337 | # no format subfolders 338 | else: 339 | # create the familyName folder if missing 340 | if not os.path.exists(familyNamePath): 341 | os.makedirs(familyNamePath) 342 | 343 | # add path to dict 344 | if familyName not in folders: 345 | folders[familyName] = {} 346 | folders[familyName][formt] = familyNamePath 347 | return folders 348 | 349 | def getFamilyNameForInstance(self, instance): 350 | if 'familyName' in instance.customParameters: 351 | return instance.customParameters['familyName'] 352 | elif Glyphs3 and instance.familyName: 353 | return instance.familyName 354 | else: 355 | return instance.font.familyName 356 | 357 | def hasNestedComponents(self, font): 358 | for glyph in font.glyphs: 359 | for layer in glyph.layers: 360 | if self.nestedComponents(layer): 361 | return True 362 | 363 | def nestedComponents(self, layer): 364 | componentsInComponents = [c.componentLayer.components for c in layer.components] 365 | return any(componentsInComponents) 366 | 367 | def doUnnestNestedComponents(self, font): 368 | # From @mekkablue’s UnnestComponents plugin 369 | for glyph in font.glyphs: 370 | for layer in glyph.layers: 371 | while self.nestedComponents(layer): 372 | for c in layer.components: 373 | if c.componentLayer.components: 374 | c.decompose() 375 | 376 | def getStaticSettings(self, font): 377 | staticSettings = [] 378 | exportDefault = False 379 | for i, instance in enumerate(font.instances): 380 | if 'Static Setting' in instance.name: 381 | staticSettings.append(i) 382 | if '+' in instance.name: 383 | exportDefault = True 384 | if not staticSettings: 385 | exportDefault = True 386 | return staticSettings, exportDefault 387 | 388 | def applyStaticSetting(self, font, staticSettingIndex): 389 | sourceInstance = font.instances[staticSettingIndex] 390 | 391 | # Copy the custom parameters from the instance 392 | for i, instance in enumerate(font.instances): 393 | # skip variable instances 394 | if instance.type == INSTANCETYPEVARIABLE: 395 | continue 396 | # skip the source instance itself 397 | if i == staticSettingIndex: 398 | continue 399 | 400 | # copy properties 401 | for prop in sourceInstance.properties: 402 | currentProp = instance.propertyForName_(prop.key) 403 | newValue = None 404 | if '->' in prop.value: # this will only replace if property found, skips otherwise except for familyName (WIP: needs a better logic!) 405 | fromValue, toValue = prop.value.split('->') 406 | if currentProp: 407 | newValue = str(currentProp.value).replace(fromValue, toValue) 408 | instance.setProperty_value_languageTag_(prop.key, newValue, None) 409 | elif prop.key == 'familyNames': 410 | newValue = str(font.familyName).replace(fromValue, toValue) 411 | instance.setProperty_value_languageTag_(prop.key, newValue, None) 412 | else: 413 | newValue = prop.value 414 | instance.setProperty_value_languageTag_(prop.key, newValue, None) 415 | 416 | # copy custom parameters 417 | for p in sourceInstance.customParameters: 418 | instance.addCustomParameter_(p.copy()) 419 | 420 | def getLinkedInstance(self, font, instance): 421 | for inst in font.instances: 422 | if inst.familyName == instance.familyName: 423 | if inst.customParameters['temp original name'] and inst.customParameters['temp original name'] == instance.linkStyle: 424 | return inst 425 | if inst.name == instance.linkStyle: 426 | return inst 427 | 428 | def exportInstances(self, font, selectedFormats, folders): 429 | if not selectedFormats: 430 | return 431 | 432 | # set up the progress bar and count instances and formats 433 | activeInstances = [instance for instance in font.instances if instance.active and (Glyphs3 is False or (('Variable' in selectedFormats or 'VariableWEB' in selectedFormats) or instance.type != INSTANCETYPEVARIABLE))] 434 | 435 | # count formats and total fonts to export 436 | formatsCount = 0 437 | for frmt in selectedFormats: 438 | if frmt == 'WEB' and not self.postProcessWEB: 439 | formatsCount += 2 440 | elif frmt == 'VariableWEB' and not self.postProcessVariableWEB: 441 | formatsCount += 2 442 | elif 'WEB' not in frmt: 443 | formatsCount += 1 444 | totalCount = len(activeInstances) * formatsCount 445 | currentCount = 0 446 | 447 | # export 448 | for formt in selectedFormats: 449 | 450 | # skip WEB if it will be doen in post 451 | if formt == 'WEB' and self.postProcessWEB: 452 | continue 453 | if formt == 'VariableWEB' and self.postProcessVariableWEB: 454 | continue 455 | 456 | # get format 457 | if formt == 'OTF': 458 | frmt = OTF 459 | elif formt == 'TTF': 460 | frmt = TTF 461 | elif formt == 'WEB': 462 | # frmt = OTF if getattr(self.w, 'outlines' + formt).get() else TTF 463 | frmt = self.getWebOutlines() 464 | else: # 'Variable' or 'VariableWEB' 465 | frmt = VARIABLE 466 | 467 | # get parameters 468 | containers = [PLAIN] if 'WEB' not in formt else [WOFF, WOFF2] 469 | removeOverlap = getattr(self.w, 'overlaps' + formt).get() if 'Variable' not in formt else False 470 | autohint = getattr(self.w, 'autohint' + formt).get() 471 | 472 | for instance in activeInstances: 473 | # format is variable => skip non-variable instances 474 | if 'Variable' in formt: 475 | if Glyphs3 and instance.type != INSTANCETYPEVARIABLE: 476 | continue 477 | # format is not variable => skip variable instances 478 | elif Glyphs3 and instance.type == INSTANCETYPEVARIABLE: 479 | continue 480 | 481 | # get familyName for instance 482 | familyName = self.getFamilyNameForInstance(instance) 483 | 484 | # get export path / folder 485 | try: 486 | exportPath = folders[familyName][formt] 487 | # check if the export folder exists 488 | if not os.path.exists(exportPath): 489 | print('Couldn’t find the folder for %s - %s - %s' % (familyName, instance.name, formt)) 490 | Glyphs.showMacroWindow() 491 | return 492 | except: 493 | print('Couldn’t find the folder for %s - %s - %s' % (familyName, instance.name, formt)) 494 | Glyphs.showMacroWindow() 495 | return 496 | 497 | # export variable in Glyphs 2 498 | if Glyphs3 is False and formt == 'Variable': 499 | font.export(FontPath=exportPath, Format=VARIABLE, AutoHint=autohint) 500 | break # only export for one instance 501 | 502 | 503 | # for variable fonts, temporarily set style names from variable style name 504 | # this is for a Glyphs 3 bug: Origin and variable style linking uses instance name instead of variable style name (should be fixed in upcoming versions - noted on Dec 15 2024) 505 | if instance.type == INSTANCETYPEVARIABLE: 506 | for inst in font.instances: 507 | if inst.type != INSTANCETYPEVARIABLE and inst.variableStyleName: 508 | # instance name 509 | inst.customParameters['temp original name'] = inst.name 510 | inst.name = inst.variableStyleName 511 | # style linking 512 | if inst.linkStyle: 513 | inst.customParameters['temp original link'] = inst.linkStyle 514 | linkedInst = self.getLinkedInstance(font, inst) 515 | inst.linkStyle = linkedInst.variableStyleName if linkedInst.variableStyleName else linkedInst.name 516 | 517 | # export the instance 518 | if Glyphs.versionNumber >= 3.3: 519 | result = instance.generate(format=frmt, fontPath=exportPath, containers=containers, removeOverlap=removeOverlap, autoHint=autohint) 520 | else: 521 | result = instance.generate(Format=frmt, FontPath=exportPath, Containers=containers, RemoveOverlap=removeOverlap, AutoHint=autohint) 522 | if result is not True: 523 | Glyphs.showMacroWindow() 524 | print(result, font, instance, frmt) 525 | 526 | if instance.type == INSTANCETYPEVARIABLE: 527 | # add ttLib table 528 | fontFileName = instance.fileName().replace('otf', 'ttf') 529 | exportedPath = exportPath + '/' + fontFileName 530 | self.addSTAT(instance, exportedPath, fontFileName) 531 | 532 | # bring back origin instance name 533 | for inst in font.instances: 534 | if inst.type != INSTANCETYPEVARIABLE: 535 | if 'temp original name' in inst.customParameters: 536 | inst.name = inst.customParameters['temp original name'] 537 | del(inst.customParameters['temp original name']) 538 | if 'temp original link' in inst.customParameters: 539 | inst.linkStyle = inst.customParameters['temp original link'] 540 | del(inst.customParameters['temp original link']) 541 | 542 | 543 | # update progress bar 544 | currentCount += len(containers) 545 | self.w.progress.set(100 / totalCount * currentCount) 546 | self.w.info.set('%s/%s %s %s' % (currentCount, totalCount, formt, instance.name)) 547 | Glyphs.showNotification('Export fonts', font.familyName + ' was exported successfully.') 548 | 549 | 550 | def exportInstancesForFonts(self, fonts, selectedFormats, folders): 551 | # separate variable and static formats 552 | selectedStaticFormats, selectedVariableFormats = [], [] 553 | for formt in selectedFormats: 554 | if formt in variableFormats: 555 | selectedVariableFormats.append(formt) 556 | else: 557 | selectedStaticFormats.append(formt) 558 | 559 | 560 | for originalFont in fonts: 561 | 562 | # unnest nested components (use a copy of the font) 563 | if self.w.unnestComponents.get() and self.hasNestedComponents(originalFont): 564 | font = originalFont.copy() 565 | font.disablesAutomaticAlignment = True 566 | self.doUnnestNestedComponents(font) 567 | else: 568 | font = originalFont 569 | 570 | # get static settings 571 | staticSettings, exportDefault = self.getStaticSettings(font) 572 | 573 | # export default static + variable 574 | self.exportInstances(font, selectedFormats if exportDefault else selectedVariableFormats, folders) 575 | 576 | # export additional static settings 577 | for staticSetting in staticSettings: 578 | staticSettingsFont = font.copy() 579 | self.applyStaticSetting(staticSettingsFont, staticSettingIndex = staticSetting) 580 | self.exportInstances(staticSettingsFont, selectedStaticFormats, folders) 581 | 582 | 583 | def compressFontsInFolder(self, sourcePath, exportPath, sourceFormats = ['.ttf']): 584 | list_ = os.listdir(sourcePath) 585 | # get files of the right extension 586 | sourceFiles = [] 587 | for file_ in list_: 588 | name, ext = os.path.splitext(file_) 589 | if ext.lower() in sourceFormats: 590 | sourceFiles.append(file_) 591 | 592 | totalCount = len(sourceFiles) 593 | currentCount = 0 594 | 595 | for file_ in sourceFiles: 596 | fontPath = sourcePath + '/' + file_ 597 | fontExportPath = exportPath + '/' + file_ 598 | font = ttLib.TTFont(fontPath) 599 | font.flavor = 'woff' 600 | font.save(fontExportPath.replace(ext, '.woff')) 601 | font.flavor = 'woff2' 602 | font.save(fontExportPath.replace(ext, '.woff2')) 603 | 604 | # update progress bar 605 | currentCount += 1 606 | self.w.progress.set(100 / totalCount * currentCount) 607 | self.w.info.set('%s/%s %s %s' % (currentCount, totalCount, 'WEB compressing', file_)) 608 | Glyphs.showNotification('Export fonts', 'Compressed successfully: ' + exportPath) 609 | 610 | 611 | def runPostProcessWEB(self, selectedFormats, folders): 612 | # compress OTF/TTF in the given folder if OTF/TTF are not exported (bonus) 613 | if len(selectedFormats) == 1 and 'WEB' in selectedFormats[0] and (self.postProcessWEB or self.postProcessVariableWEB): 614 | sourcePath = self.exportPath 615 | exportPath = sourcePath + '/WEB' if self.w.subfolders.get() else sourcePath 616 | self.compressFontsInFolder(sourcePath, exportPath, sourceFormats=['.otf', '.ttf']) 617 | 618 | # compress the exported files 619 | else: 620 | # get post process checkboxes 621 | for formt in selectedFormats: 622 | if self.postProcessWEB and formt == 'WEB': 623 | for familyName, formatSubfolders in folders.items(): 624 | if formt in formatSubfolders: 625 | PSOutlines = self.getWebOutlines() == OTF 626 | self.compressFontsInFolder( 627 | sourcePath = formatSubfolders['OTF' if PSOutlines else 'TTF'], 628 | exportPath = formatSubfolders['WEB'], 629 | sourceFormats = ['.otf'] if PSOutlines else ['.ttf']) 630 | 631 | elif self.postProcessVariableWEB and formt == 'VariableWEB': 632 | for familyName, formatSubfolders in folders.items(): 633 | if formt in formatSubfolders: 634 | self.compressFontsInFolder( 635 | sourcePath = formatSubfolders['Variable'], 636 | exportPath = formatSubfolders['VariableWEB'], 637 | sourceFormats = ['.ttf']) 638 | 639 | 640 | def addSTAT(self, instance, fontPath, fontFileName): 641 | # @mekkablue’s Read and Write STAT Axis Values (OTVAR) 642 | font = ttLib.TTFont(fontPath) 643 | parameterName = "Axis Values" 644 | 645 | def designAxisRecordDict(statTable): 646 | axes = [] 647 | for axis in statTable.DesignAxisRecord.Axis: 648 | axes.append({ 649 | "nameID": axis.AxisNameID, 650 | "tag": axis.AxisTag, 651 | "ordering": axis.AxisOrdering, 652 | }) 653 | # print(f"- {axis.AxisTag} axis: AxisNameID {axis.AxisNameID}, AxisOrdering {axis.AxisOrdering}") 654 | return axes 655 | 656 | def nameDictAndHighestNameID(nameTable): 657 | nameDict = {} 658 | highestID = 255 659 | for nameTableEntry in nameTable.names: 660 | nameID = nameTableEntry.nameID 661 | if nameID > highestID: 662 | highestID = nameID 663 | nameValue = nameTableEntry.toStr() 664 | if nameValue not in nameDict.keys(): 665 | nameDict[nameValue] = nameID 666 | return nameDict, highestID 667 | 668 | def parameterToSTAT(variableFontExport, font, fontpath, fontFileName): 669 | nameTable = font["name"] 670 | nameDict, highestID = nameDictAndHighestNameID(nameTable) 671 | statTable = font["STAT"].table 672 | axes = designAxisRecordDict(statTable) 673 | 674 | newAxisValues = [] 675 | for parameter in variableFontExport.customParameters: 676 | if parameter.name == parameterName and parameter.active: 677 | statCode = parameter.value 678 | # print(f"\n👨🏼‍🏫 Parsing parameter value: {statCode.strip()}") 679 | 680 | axisTag, axisValueCode = statCode.split(";") 681 | axisTag = axisTag.strip() 682 | for i, axisInfo in enumerate(axes): 683 | if axisTag == axisInfo["tag"]: 684 | axisIndex = i 685 | break 686 | 687 | if len(axisTag) > 4: 688 | # print(f"⚠️ axis tag ‘{axisTag}’ is too long, will shorten to first 4 characters.") 689 | axisTag = axisTag[:4] 690 | 691 | for entryCode in axisValueCode.split(","): 692 | newAxisValue = ttLib.tables.otTables.AxisValue() 693 | entryValues, entryName = entryCode.split("=") 694 | entryName = entryName.strip() 695 | entryFlags = 0 696 | if entryName.endswith("*"): 697 | entryFlags = 2 698 | entryName = entryName[:-1] 699 | 700 | if entryName in nameDict.keys(): 701 | entryValueNameID = nameDict[entryName] 702 | else: 703 | # add name entry: 704 | highestID += 1 705 | entryValueNameID = highestID 706 | nameTable.addName(entryName, platforms=((3, 1, 1033), ), minNameID=highestID - 1) 707 | nameDict[entryName] = entryValueNameID 708 | # print(f"- Adding nameID {entryValueNameID}: ‘{entryName}’") 709 | 710 | if ">" in entryValues: # Format 3, STYLE LINKING 711 | entryValue, entryLinkedValue = [float(x.strip()) for x in entryValues.split(">")] 712 | newAxisValue.Format = 3 713 | newAxisValue.AxisIndex = axisIndex 714 | newAxisValue.ValueNameID = entryValueNameID 715 | newAxisValue.Flags = entryFlags 716 | newAxisValue.Value = entryValue 717 | newAxisValue.LinkedValue = entryLinkedValue 718 | # print(f"- AxisValue {axisTag} ‘{entryName}’, Format {newAxisValue.Format}, AxisIndex {newAxisValue.AxisIndex}, ValueNameID {newAxisValue.ValueNameID}, Flags {newAxisValue.Flags}, Value {newAxisValue.Value}, LinkedValue {newAxisValue.LinkedValue}") 719 | 720 | elif ":" in entryValues: # Format 2, RANGE 721 | entryRangeMinValue, entryNominalValue, entryRangeMaxValue = [float(x.strip()) for x in entryValues.split(":")] 722 | newAxisValue.Format = 2 723 | newAxisValue.AxisIndex = axisIndex 724 | newAxisValue.ValueNameID = entryValueNameID 725 | newAxisValue.Flags = entryFlags 726 | newAxisValue.RangeMinValue = entryRangeMinValue 727 | newAxisValue.NominalValue = entryNominalValue 728 | newAxisValue.RangeMaxValue = entryRangeMaxValue 729 | # print(f"- AxisValue {axisTag} ‘{entryName}’, Format {newAxisValue.Format}, AxisIndex {newAxisValue.AxisIndex}, ValueNameID {newAxisValue.ValueNameID}, Flags {newAxisValue.Flags}, RangeMinValue {newAxisValue.RangeMinValue}, NominalValue {newAxisValue.NominalValue}, RangeMaxValue {newAxisValue.RangeMaxValue}") 730 | 731 | else: # Format 1, DISCRETE SPOT 732 | entryValue = float(entryValues.strip()) 733 | newAxisValue.Format = 1 734 | newAxisValue.AxisIndex = axisIndex 735 | newAxisValue.ValueNameID = entryValueNameID 736 | newAxisValue.Flags = entryFlags 737 | newAxisValue.Value = entryValue 738 | # print(f"- AxisValue {axisTag} ‘{entryName}’, Format {newAxisValue.Format}, AxisIndex {newAxisValue.AxisIndex}, ValueNameID {newAxisValue.ValueNameID}, Flags {newAxisValue.Flags}, Value {newAxisValue.Value}") 739 | 740 | newAxisValues.append(newAxisValue) 741 | 742 | # print(f"\n✅ Overwriting STAT AxisValues with {len(newAxisValues)} entries...") 743 | statTable.AxisValueArray.AxisValue = newAxisValues 744 | font.save(fontpath, reorderTables=False) 745 | # print(f"💾 Saved file: {fontFileName}") 746 | 747 | if instance.customParameters[parameterName]: 748 | parameterToSTAT(instance, font, fontPath, fontFileName) 749 | 750 | 751 | 752 | def run(self, sender): 753 | # check if the export folder exists 754 | if not os.path.exists(self.exportPath): 755 | self.w.info.set('That folder doesn’t exist!') 756 | return 757 | 758 | # deactivate the run button to avoid accidental clicks 759 | self.w.run.enable(0) 760 | 761 | # update the current default path 762 | Glyphs.defaults['OTFExportPath'] = self.exportPath 763 | 764 | # get fonts (all or the current one) 765 | fonts = Glyphs.fonts if self.w.exportAll.get() else [Glyphs.font] 766 | 767 | # if there are no fonts, notify about it 768 | if not fonts or None in fonts: 769 | self.w.info.set('Could not export: no fonts are open.') 770 | return 771 | 772 | # get all selected formats from UI 773 | selectedFormats = [frmt for frmt in formats if getattr(self.w, 'exportCheckBox' + frmt).get()] 774 | 775 | # get familyNames from all fonts and all instances’ custom parameters 776 | familyNames = self.getAllFamilyNames(fonts, selectedFormats) 777 | 778 | # create folders 779 | folders = self.createFolders(familyNames, selectedFormats) 780 | 781 | # export instances 782 | self.exportInstancesForFonts(fonts, selectedFormats, folders) 783 | 784 | # post process WEB 785 | self.runPostProcessWEB(selectedFormats, folders) 786 | 787 | # reset progress bar 788 | self.w.info.set('') 789 | 790 | # activate the run button back 791 | self.w.run.enable(1) 792 | 793 | ExportToAllFormats() -------------------------------------------------------------------------------- /App/Fit Zoom.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Fit Zoom 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Fit text in the current tab to full screen. Works weirdly in Glyphs 3. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Glyphs.clearLog() 11 | font = Glyphs.font 12 | tab = font.currentTab 13 | viewPort = tab.viewPort 14 | 15 | # check if text tool is selected 16 | if Glyphs.currentDocument.windowController().toolDrawDelegate().className() in ['GlyphsToolText', 'GlyphsToolHand']: 17 | textTool = True 18 | else: 19 | textTool = False 20 | 21 | 22 | # text/hand tool > fit all 23 | if textTool: 24 | # set scale to 1 25 | tab.scale = 1 26 | # set margin (relative) 27 | M = tab.bounds.size.width / 30 28 | # get screen ratio 29 | screenRatio = viewPort.size.width / viewPort.size.height 30 | # get text ratio 31 | textRatio = tab.bounds.size.width / tab.bounds.size.height 32 | 33 | # fit by width 34 | if textRatio > screenRatio: 35 | # width 36 | viewPort.size.width = tab.bounds.size.width + M * 2 37 | # x 38 | viewPort.origin.x = -M 39 | # y (centered) 40 | viewPort.origin.y = -viewPort.size.height / 2 - tab.bounds.size.height / 2 41 | 42 | # fit by height 43 | else: 44 | # height 45 | viewPort.size.height = tab.bounds.size.height + M * 2 46 | # y 47 | viewPort.origin.y = -tab.bounds.size.height 48 | # x (centered) 49 | viewPort.origin.x = -viewPort.size.width / 2 + tab.bounds.size.width / 2 50 | 51 | # apply changes 52 | tab.viewPort = viewPort 53 | 54 | 55 | # other tools > fit current layer 56 | else: 57 | if font.selectedLayers: 58 | # set scale to 1 59 | tab.scale = 1 60 | 61 | layer = font.selectedLayers[0] 62 | # get layer's origin position in tab 63 | activePos = font.parent.windowController().activeEditViewController().graphicView().activePosition() 64 | # set margin (absolute) 65 | M = 50 66 | # get screen ratio 67 | screenRatio = viewPort.size.width / viewPort.size.height 68 | # get current layer ratio (descender-ascender) 69 | layerHeight = abs(layer.master.descender) + layer.master.ascender 70 | textRatio = layer.width / layerHeight 71 | 72 | # # fit by width 73 | if textRatio > screenRatio: 74 | # width 75 | viewPort.size.width = layer.width + M * 2 76 | 77 | # fit by height 78 | else: 79 | # height 80 | viewPort.size.height = layerHeight + M * 2 81 | 82 | # x (centered) 83 | viewPort.origin.x = activePos.x - viewPort.size.width / 2 + layer.width / 2 84 | # y (centered) 85 | viewPort.origin.y = -viewPort.size.height / 2 + activePos.y + (layer.master.ascender + layer.master.descender) / 2 86 | 87 | # apply changes 88 | tab.viewPort = viewPort 89 | -------------------------------------------------------------------------------- /App/Floating Macro Panel.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Floating Macro Panel 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggles Macro panel floating. 6 | """ 7 | 8 | from AppKit import NSApp, NSFloatingWindowLevel, NSNormalWindowLevel 9 | from GlyphsApp import Glyphs 10 | 11 | Glyphs.showMacroWindow() 12 | for window in NSApp.windows(): 13 | if window.className() == 'GSMacroWindow': 14 | # make normal 15 | if window.level() == 3: 16 | window.setLevel_(NSNormalWindowLevel) 17 | print('Macro panel is not floating now') 18 | # make floating 19 | else: 20 | window.setLevel_(NSFloatingWindowLevel) 21 | print('Macro panel is floating now') 22 | break 23 | -------------------------------------------------------------------------------- /App/Next Layer in Selection.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Next Layer in Selection 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | import traceback 5 | from copy import copy 6 | 7 | __doc__ = """ 8 | Switches to the next layer in all selected glyphs in a tab. 9 | Uses the first glyph’s layers to determine “next layer”. 10 | """ 11 | 12 | from GlyphsApp import Glyphs 13 | 14 | direction = 1 # -1 = previous, 1 = next 15 | 16 | 17 | def get_prev_or_next_layer(layer, direction): 18 | try: 19 | layers = layer.parent.sortedLayers() 20 | layer_index = layers.indexOfObject_(layer) + direction 21 | if layer_index < 0: 22 | layer_index = len(layers) - 1 23 | elif layer_index >= len(layers): 24 | layer_index = 0 25 | new_layer = layers[layer_index] 26 | return new_layer 27 | except: 28 | return None 29 | 30 | 31 | def apply_layer_to_selected_glyphs(new_layer, selected_layers): 32 | new_selected_layers = [] 33 | for layer in selected_layers: 34 | l = layer.parent.layerForName_(new_layer.name) 35 | new_selected_layers.append(l if l else layer) 36 | return new_selected_layers 37 | 38 | 39 | def set_master(font, tab, text_cursor, text_range, toggle, master_index): 40 | # select the layers 41 | tab.textCursor = text_cursor 42 | tab.textRange = text_range 43 | # set master index 44 | font.masterIndex = master_index + toggle 45 | font.masterIndex -= toggle 46 | 47 | 48 | def set_master_layers_to_master(font, tab, master): 49 | # get user's selection to reset later 50 | current_text_cursor = tab.textCursor 51 | current_text_range = tab.textRange 52 | master_index = font.masters.index(master) 53 | 54 | # toggle master to some other master and back, otherwise it doesn't apply 55 | toggle = -1 if 0 < master_index else 1 56 | 57 | # select old master layers and apply master 58 | text_cursor = None 59 | text_range = 0 60 | for i, layer in enumerate(tab.layers): 61 | if layer.isMasterLayer and layer.master == master: 62 | if text_cursor is None: 63 | text_cursor = i 64 | text_range += 1 65 | else: 66 | if text_cursor is not None: 67 | set_master(font, tab, text_cursor, text_range, toggle, master_index) 68 | # reset selection 69 | text_cursor = None 70 | text_range = 0 71 | if text_cursor is not None: 72 | set_master(font, tab, text_cursor, text_range, toggle, master_index) 73 | 74 | # set original user's selection 75 | tab.textCursor = current_text_cursor 76 | tab.textRange = current_text_range 77 | 78 | 79 | def text_range_for_layers(layers): 80 | return sum(1 if layer.parent.unicode else 2 for layer in layers) 81 | 82 | 83 | def switch_layers(direction=1): 84 | font = Glyphs.font 85 | if not font or not font.currentTab or not font.selectedLayers: 86 | return 87 | # get initial tab layers 88 | tab = font.currentTab 89 | initial_tab_layers = copy(tab.layers) 90 | 91 | # get text selection 92 | selection_start = tab.layersCursor 93 | selection_end = tab.layersCursor + len(tab.selectedLayers) 94 | first_layer = tab.layers[tab.layersCursor] 95 | 96 | try: 97 | new_first_layer = get_prev_or_next_layer(first_layer, direction) 98 | except: 99 | print(traceback.format_exc()) 100 | return 101 | 102 | # apply the new layer to all selected glyphs; skip if not possible 103 | selected_layers = tab.selectedLayers 104 | new_selected_layers = apply_layer_to_selected_glyphs(new_first_layer, selected_layers) 105 | 106 | # apply layers to the whole tab or only selected layers 107 | if tab.textRange: 108 | new_tab_layers = initial_tab_layers[:selection_start] + new_selected_layers + initial_tab_layers[selection_end:] 109 | else: 110 | new_tab_layers = initial_tab_layers[:selection_start] + new_selected_layers + initial_tab_layers[selection_start + 1:] 111 | tab.layers = new_tab_layers 112 | 113 | set_master_layers_to_master(font, tab, font.masters[tab.masterIndex]) 114 | 115 | 116 | switch_layers(direction) -------------------------------------------------------------------------------- /App/Previous Layer in Selection.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Previous Layer in Selection 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | import traceback 5 | from copy import copy 6 | 7 | __doc__ = """ 8 | Switches to the previous layer in all selected glyphs in a tab. 9 | Uses the first glyph’s layers to determine “next layer”. 10 | """ 11 | 12 | from GlyphsApp import Glyphs 13 | 14 | direction = -1 # -1 = previous, 1 = next 15 | 16 | 17 | def get_prev_or_next_layer(layer, direction): 18 | try: 19 | layers = layer.parent.sortedLayers() 20 | layer_index = layers.indexOfObject_(layer) + direction 21 | if layer_index < 0: 22 | layer_index = len(layers) - 1 23 | elif layer_index >= len(layers): 24 | layer_index = 0 25 | new_layer = layers[layer_index] 26 | return new_layer 27 | except: 28 | return None 29 | 30 | 31 | def apply_layer_to_selected_glyphs(new_layer, selected_layers): 32 | new_selected_layers = [] 33 | for layer in selected_layers: 34 | l = layer.parent.layerForName_(new_layer.name) 35 | new_selected_layers.append(l if l else layer) 36 | return new_selected_layers 37 | 38 | 39 | def set_master(font, tab, text_cursor, text_range, toggle, master_index): 40 | # select the layers 41 | tab.textCursor = text_cursor 42 | tab.textRange = text_range 43 | # set master index 44 | font.masterIndex = master_index + toggle 45 | font.masterIndex -= toggle 46 | 47 | 48 | def set_master_layers_to_master(font, tab, master): 49 | # get user's selection to reset later 50 | current_text_cursor = tab.textCursor 51 | current_text_range = tab.textRange 52 | master_index = font.masters.index(master) 53 | 54 | # toggle master to some other master and back, otherwise it doesn't apply 55 | toggle = -1 if 0 < master_index else 1 56 | 57 | # select old master layers and apply master 58 | text_cursor = None 59 | text_range = 0 60 | for i, layer in enumerate(tab.layers): 61 | if layer.isMasterLayer and layer.master == master: 62 | if text_cursor is None: 63 | text_cursor = i 64 | text_range += 1 65 | else: 66 | if text_cursor is not None: 67 | set_master(font, tab, text_cursor, text_range, toggle, master_index) 68 | # reset selection 69 | text_cursor = None 70 | text_range = 0 71 | if text_cursor is not None: 72 | set_master(font, tab, text_cursor, text_range, toggle, master_index) 73 | 74 | # set original user's selection 75 | tab.textCursor = current_text_cursor 76 | tab.textRange = current_text_range 77 | 78 | 79 | def text_range_for_layers(layers): 80 | return sum(1 if layer.parent.unicode else 2 for layer in layers) 81 | 82 | 83 | def switch_layers(direction=1): 84 | font = Glyphs.font 85 | if not font or not font.currentTab or not font.selectedLayers: 86 | return 87 | # get initial tab layers 88 | tab = font.currentTab 89 | initial_tab_layers = copy(tab.layers) 90 | 91 | # get text selection 92 | selection_start = tab.layersCursor 93 | selection_end = tab.layersCursor + len(tab.selectedLayers) 94 | first_layer = tab.layers[tab.layersCursor] 95 | 96 | try: 97 | new_first_layer = get_prev_or_next_layer(first_layer, direction) 98 | except: 99 | print(traceback.format_exc()) 100 | return 101 | 102 | # apply the new layer to all selected glyphs; skip if not possible 103 | selected_layers = tab.selectedLayers 104 | new_selected_layers = apply_layer_to_selected_glyphs(new_first_layer, selected_layers) 105 | 106 | # apply layers to the whole tab or only selected layers 107 | if tab.textRange: 108 | new_tab_layers = initial_tab_layers[:selection_start] + new_selected_layers + initial_tab_layers[selection_end:] 109 | else: 110 | new_tab_layers = initial_tab_layers[:selection_start] + new_selected_layers + initial_tab_layers[selection_start + 1:] 111 | tab.layers = new_tab_layers 112 | 113 | set_master_layers_to_master(font, tab, font.masters[tab.masterIndex]) 114 | 115 | 116 | switch_layers(direction) -------------------------------------------------------------------------------- /App/Toggle Axis 1.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Axis 1 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Toggles along masters across the 1st axis. 5 | """ 6 | 7 | from ToggleAxis import toggleAxis 8 | 9 | toggleAxis(0) -------------------------------------------------------------------------------- /App/Toggle Axis 2.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Axis 2 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Toggles along masters across the 2nd axis. 5 | """ 6 | 7 | from ToggleAxis import toggleAxis 8 | 9 | toggleAxis(1) -------------------------------------------------------------------------------- /App/Toggle Axis 3.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Axis 3 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Toggles along masters across the 3rd axis. 5 | """ 6 | 7 | from ToggleAxis import toggleAxis 8 | 9 | toggleAxis(2) -------------------------------------------------------------------------------- /App/Toggle Axis 4.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Axis 4 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Toggles along masters across the 4th axis. 5 | """ 6 | 7 | from ToggleAxis import toggleAxis 8 | 9 | toggleAxis(3) -------------------------------------------------------------------------------- /App/Toggle Italic.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Italic 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Toggles along masters across the Italic or Slant axis. 5 | """ 6 | 7 | from ToggleAxis import toggleAxis 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def getItalicAxis(): 12 | for i, axis in enumerate(Glyphs.font.axes): 13 | axisTag = axis['Tag'] if Glyphs.versionNumber < 3 else axis.axisTag 14 | if axisTag in ['ital', 'slnt']: 15 | axis = i 16 | return axis 17 | 18 | toggleAxis(getItalicAxis()) -------------------------------------------------------------------------------- /App/ToggleAxis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __doc__ = """ 3 | Toggles along masters across the 1st axis. 4 | """ 5 | 6 | from GlyphsApp import Glyphs, GSControlLayer, GSFontMaster 7 | from copy import copy 8 | 9 | 10 | def getNextMaster(master, relatedLayers, AXIS): 11 | for layer in relatedLayers: 12 | if getAxisValue(layer, AXIS) > master.axes[AXIS]: 13 | return layer 14 | return relatedLayers[0] 15 | 16 | 17 | def getRelatedMasters(selectedMaster, AXIS): 18 | # find "related" layers along the axis 19 | relatedMasters = [] 20 | for master in selectedMaster.font.masters: 21 | if master != selectedMaster: 22 | related = True 23 | for i, axis in enumerate(master.axes): 24 | if i != AXIS: 25 | if axis != selectedMaster.axes[i]: 26 | related = False 27 | if related is True: 28 | relatedMasters.append(master) 29 | return relatedMasters 30 | 31 | 32 | def toggleMaster(master, AXIS): 33 | relatedMasters = getRelatedMasters(master, AXIS) 34 | nextMaster = getNextMaster(master, relatedMasters) 35 | 36 | tab = master.font.currentTab 37 | if tab: 38 | setMasterLayersToMaster(tab, newMaster=nextMaster, currentMaster=master) 39 | else: 40 | master.font.masterIndex = master.font.masters.index(nextMaster) 41 | 42 | 43 | def setMaster(font, tab, textCursor, textRange, toggle, newMasterIndex): 44 | # select the layers 45 | tab.textCursor = textCursor 46 | tab.textRange = textRange 47 | # set master index 48 | font.masterIndex = newMasterIndex + toggle 49 | font.masterIndex -= toggle 50 | 51 | 52 | def setMasterLayersToMaster(tab, newMaster, currentMaster=None): 53 | # get user's selection to reset later 54 | currentTextCursor = tab.textCursor 55 | currentTextRange = tab.textRange 56 | newMasterIndex = font.masters.index(newMaster) 57 | if currentMaster is None: 58 | currentMaster = newMaster 59 | # toggle master to some other master and back, otherwise it doesn't apply 60 | if newMaster == currentMaster: 61 | if 0 < newMasterIndex: 62 | toggle = -1 63 | else: 64 | toggle = 1 65 | else: 66 | toggle = 0 67 | 68 | # select old master layers and apply master 69 | textCursor = None 70 | textRange = 0 71 | for i, layer in enumerate(tab.layers): 72 | if layer.isMasterLayer and layer.master == currentMaster: 73 | if textCursor is None: 74 | textCursor = i 75 | textRange += 1 76 | else: 77 | if textCursor is not None: 78 | setMaster(font, tab, textCursor, textRange, toggle, newMasterIndex) 79 | # reset selection 80 | textCursor = None 81 | textRange = 0 82 | if textCursor is not None: 83 | setMaster(font, tab, textCursor, textRange, toggle, newMasterIndex) 84 | 85 | # set original user's selection 86 | tab.textCursor = currentTextCursor 87 | tab.textRange = currentTextRange 88 | 89 | 90 | def getAxisValue(layer, axis): 91 | axes = layer.tempDataForKey_("axes") 92 | if axes is None: 93 | axes = getLayerAxes(layer) 94 | return axes[axis] 95 | 96 | 97 | def getLayerAxes(layer): 98 | if isinstance(layer, GSFontMaster): 99 | return layer.axes 100 | layerAxes = [] 101 | if Glyphs.versionNumber >= 3: 102 | coordinates = layer.attributes["coordinates"] 103 | if coordinates: 104 | axes = layer.axes() 105 | for axis in axes: 106 | coordinate = coordinates.get(axis.axisId, 0) 107 | layerAxes.append(coordinate) 108 | else: 109 | if '{' in layer.name: 110 | axesStr = layer.name.replace('{', '').replace('}', '').replace(",", " ").split() 111 | for axis in axesStr: 112 | layerAxes.append(float(axis)) 113 | if not layerAxes: 114 | layerAxes = layer.master.axes 115 | # layer.setTempData_forKey_(layerAxes) 116 | layer.setTempData_forKey_(layerAxes, "axes") 117 | return layerAxes 118 | 119 | 120 | def getRelatedLayers(specialLayer, AXIS): 121 | selectedLayerAxes = getLayerAxes(specialLayer) 122 | 123 | # ignore non {} layers 124 | if not selectedLayerAxes: 125 | return 126 | 127 | # get related layers 128 | relatedLayers = [] 129 | for layer in specialLayer.parent.layers: 130 | 131 | # skip the layer itself 132 | if layer == specialLayer: 133 | continue 134 | 135 | # get layer axes 136 | layerAxes = getLayerAxes(layer) 137 | 138 | # check if all axes match (except for the AXIS) 139 | related = True 140 | for i, axis in enumerate(layerAxes): 141 | if i != AXIS: 142 | if axis != getAxisValue(specialLayer, i): 143 | related = False 144 | 145 | # append related layers 146 | if related is True: 147 | relatedLayers.append(layer) 148 | 149 | # sort by the AXIS value 150 | relatedLayers.sort(key=lambda layer: getAxisValue(layer, AXIS)) 151 | return relatedLayers 152 | 153 | 154 | def getNextSpecialLayerName(specialLayer, AXIS): 155 | relatedLayers = getRelatedLayers(specialLayer, AXIS) 156 | 157 | # {} layers 158 | if relatedLayers: 159 | nextSpecialLayer = getNextMaster(specialLayer.master, relatedLayers, AXIS) 160 | return nextSpecialLayer.name 161 | 162 | # non {} layers 163 | else: 164 | return specialLayer.name 165 | 166 | 167 | def getNextMasterId(font, masterId, AXIS): 168 | master = font.masters[masterId] 169 | relatedMasters = getRelatedMasters(master, AXIS) 170 | nextMaster = getNextMaster(master, relatedMasters, AXIS) 171 | return nextMaster.id 172 | 173 | 174 | def toggleMasterInTab(master, tab, AXIS): 175 | # get next master for current master and save it 176 | # go through each layer and if it’s different from master, find its next layer id, and save it too and swap. 177 | # in the line above also check in the list instead of finding again. 178 | 179 | cachedNextLayers = {} 180 | tempTabLayers = copy(tab.layers) 181 | 182 | # get next master to the tab master 183 | nextMasterId = getNextMasterId(master.font, master.id, AXIS) 184 | nextMaster = master.font.masters[nextMasterId] 185 | 186 | # cache next master and toggle tab master 187 | cachedNextLayers[master.id] = nextMasterId 188 | master.font.masterIndex = master.font.masters.index(nextMaster) 189 | 190 | # get layer selection 191 | selectionStart = tab.layersCursor 192 | selectionEnd = tab.layersCursor + len(tab.selectedLayers) 193 | 194 | # find layers different from tab master (check in the original/temp list) 195 | for i, layer in enumerate(tempTabLayers): 196 | 197 | # layer not selected (if any selection) 198 | if not ( 199 | tab.textRange == 0 200 | or selectionStart <= i < selectionEnd 201 | or (selectionStart == selectionEnd and i == selectionStart) 202 | ): 203 | continue 204 | 205 | # other master layer 206 | elif layer.isMasterLayer and layer.layerId != master.id: 207 | # get next layer (either from cache or do cache it) 208 | nextLayerId = cachedNextLayers.get(layer.layerId) 209 | if nextLayerId is None: 210 | nextLayerId = getNextMasterId(layer.layerId, AXIS) 211 | cachedNextLayers[layer.layerId] = nextLayerId 212 | nextLayer = layer.parent.layers[nextLayerId] 213 | tempTabLayers[i] = nextLayer 214 | 215 | # special layer 216 | elif layer.isSpecialLayer: 217 | nextLayerName = cachedNextLayers.get(layer.name) 218 | if nextLayerName is None: 219 | nextLayerName = getNextSpecialLayerName(layer, AXIS) 220 | cachedNextLayers[layer.name] = nextLayerName 221 | nextLayer = layer.parent.layers[nextLayerName] 222 | tempTabLayers[i] = nextLayer 223 | 224 | # control layer, skip 225 | elif isinstance(layer, GSControlLayer): 226 | continue 227 | 228 | # current master layer, just append 229 | else: 230 | tempTabLayers[i] = layer.parent.layers[nextMasterId] 231 | 232 | # apply new layers only if any differ from tab master OR if any selection 233 | if len(cachedNextLayers) > 1 or 0 < tab.textRange < len(tab.layers): 234 | tab.layers = tempTabLayers 235 | setMasterLayersToMaster(tab, nextMaster) 236 | 237 | 238 | def getViewPortPosition(viewPort): 239 | viewPortX = viewPort.origin.x 240 | viewPortY = viewPort.origin.y 241 | return viewPortX, viewPortY 242 | 243 | 244 | def setViewPortPosition(tab, viewPort, x, y): 245 | viewPort.origin.x = x 246 | viewPort.origin.y = y 247 | tab.viewPort = viewPort 248 | 249 | 250 | def toggleAxis(AXIS): 251 | font = Glyphs.font 252 | tab = font.currentTab 253 | selectedMaster = font.selectedFontMaster 254 | 255 | if AXIS is None or AXIS >= len(font.axes): 256 | print("Axis %s not found" % AXIS) 257 | return 258 | 259 | if not tab: 260 | toggleMaster(selectedMaster, AXIS) 261 | 262 | else: 263 | # get viewport position 264 | viewPort = tab.viewPort 265 | viewPortX, viewPortY = getViewPortPosition(viewPort) 266 | 267 | # toggle layers and masters 268 | toggleMasterInTab(selectedMaster, tab, AXIS) 269 | 270 | # restore viewport position 271 | setViewPortPosition(tab, viewPort, viewPortX, viewPortY) -------------------------------------------------------------------------------- /Bug fixing/Dangerous Offcurves.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Dangerous Offcurves 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | 5 | # -------------------- 6 | # This script opens a new tab with layers if there are offcurve points 7 | # closer to their curve segment than THRESHOLD VALUE 8 | # -------------------- 9 | 10 | import objc 11 | from GlyphsApp import Glyphs, GSFont 12 | from AppKit import NSBundle, NSPoint 13 | 14 | # --- c function from Glyphs to check line to curve intersection 15 | bundle = NSBundle.bundleForClass_(GSFont) 16 | objc.loadBundleFunctions(bundle, globals(), [("GSIntersectBezier3Line", b"@{CGPoint=dd}{CGPoint=dd}{CGPoint=dd}{CGPoint=dd}{CGPoint=dd}{CGPoint=dd}")]) 17 | # intersections = GSIntersectBezier3Line(NSPoint(0, 0), NSPoint(10, 10), NSPoint(10, 30), NSPoint(0, 40), NSPoint(60, 20), NSPoint(-10000, 20)) 18 | 19 | Glyphs.clearLog() 20 | font = Glyphs.font 21 | 22 | 23 | # define how many units away can offcurve points be from the curve segment 24 | # change this value if needed: 25 | THRESHOLD = 0.05 26 | 27 | 28 | def getHorizontalIntersection(point, a, b, c, d): 29 | try: 30 | # intersecting line 31 | e = NSPoint(-10000, point.y) 32 | f = NSPoint(10000, point.y) 33 | intersections = GSIntersectBezier3Line(a, b, c, d, e, f) 34 | intersection = intersections[0].pointValue() 35 | return intersection 36 | except: 37 | #print('Could not find horizontal intersection') 38 | return None 39 | 40 | 41 | def getVerticalIntersection(point, a, b, c, d): 42 | try: 43 | # intersecting line 44 | e = NSPoint(point.x, -10000) 45 | f = NSPoint(point.x, 10000) 46 | intersections = GSIntersectBezier3Line(a, b, c, d, e, f) 47 | intersection = intersections[0].pointValue() 48 | return intersection 49 | except: 50 | #print('Could not find vertical intersection') 51 | return None 52 | 53 | 54 | problematicLayers = [] 55 | 56 | # check in all glyphs 57 | for glyph in font.glyphs: 58 | for layer in glyph.layers: 59 | for path in layer.paths: 60 | for node in path.nodes: 61 | # get curved segment 62 | if node.type == 'offcurve' and node.prevNode.type != 'offcurve': 63 | a = (node.prevNode.x, node.prevNode.y) 64 | b = (node.x, node.y) 65 | c = (node.nextNode.x, node.nextNode.y) 66 | d = (node.nextNode.nextNode.x, node.nextNode.nextNode.y) 67 | # get intersection points for two offcurves 68 | hIntersection1 = getHorizontalIntersection(node, a, b, c, d) 69 | vIntersection1 = getVerticalIntersection(node, a, b, c, d) 70 | hIntersection2 = getHorizontalIntersection(node.nextNode, a, b, c, d) 71 | vIntersection2 = getVerticalIntersection(node.nextNode, a, b, c, d) 72 | 73 | if hIntersection1 and vIntersection1 and hIntersection2 and vIntersection2: 74 | # if either offcurve point is closer to the curve than THRESHOLD value 75 | if ( 76 | abs(hIntersection1.x - node.x) < THRESHOLD 77 | or abs(vIntersection1.y - node.y) < THRESHOLD 78 | or abs(hIntersection2.x - node.nextNode.x) < THRESHOLD 79 | or abs(vIntersection2.y - node.nextNode.y) < THRESHOLD 80 | ): 81 | # collect layers 82 | if layer not in problematicLayers: 83 | problematicLayers.append(layer) 84 | print('%s -- %s' % (layer.parent.name, layer.name)) 85 | 86 | # open a new tab with potentially problematic layers 87 | if problematicLayers: 88 | font.newTab(problematicLayers) 89 | else: 90 | print('Could not find any offcurve points that are closer to their curves than %s units' % THRESHOLD) 91 | -------------------------------------------------------------------------------- /Bug fixing/Point Counter.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Point Counter 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Counts points in all layers of the selected glyphs and reports in macro panel. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font = Glyphs.font 11 | Glyphs.clearLog() 12 | Glyphs.showMacroWindow() 13 | 14 | 15 | # Get selected glyph 16 | for layer in font.selectedLayers: 17 | glyph = layer.parent 18 | print("---------- %s ----------" % glyph.name) 19 | 20 | incompatiblePaths = {} 21 | incompatibleOffcurves = {} 22 | incompatibleCurves = {} 23 | incompatibleLines = {} 24 | 25 | # count paths, on curve and off-curve points 26 | 27 | countPointsPrev1 = -1 28 | countPointsPrev2 = -1 29 | 30 | for layer in glyph.layers: 31 | countPaths = 0 32 | countOffcurves = 0 33 | countCurves = 0 34 | countLines = 0 35 | 36 | for i, path in enumerate(layer.paths): 37 | countPaths += 1 38 | for node in path.nodes: 39 | if node.type == "offcurve": 40 | countOffcurves += 1 41 | if node.type == "curve": 42 | countCurves += 1 43 | if node.type == "line": 44 | countLines += 1 45 | 46 | countPoints = countCurves + countOffcurves + countLines 47 | 48 | if countPoints == countPointsPrev1 or countPoints == countPointsPrev2 or countPointsPrev1 == -1: 49 | test = "" 50 | else: 51 | test = "? " 52 | 53 | countPointsPrev2 = countPointsPrev1 54 | countPointsPrev1 = countPoints 55 | 56 | print("%s%s points - %s lines, %s curves, %s offcurves, %s paths - %s" % (test, countPoints, countLines, countCurves, countOffcurves, countPaths, layer.name)) 57 | print("") 58 | -------------------------------------------------------------------------------- /Features/Generate Random Alternates Feature.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Generate Random Alternates Feature 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates a Random Alternates feature (calt by default). Replaces the existing feature code, if any. 6 | Run the script and read the comment in the OT feature for an explanation. For more info, github.com/slobzheninov 7 | """ 8 | 9 | import objc 10 | from random import choice, randint 11 | from vanilla import FloatingWindow, TextBox, EditText, TextEditor, Button 12 | from GlyphsApp import Glyphs, GSClass, GSFeature 13 | 14 | font = Glyphs.font 15 | Glyphs.clearLog() 16 | 17 | 18 | comment = """# This is your random feature! 19 | # Here’s how it works: 20 | # 1. Glyphs from the selected Categories are randomly added to the selected number of Classes (called rand...). 21 | # 2. Then glyphs and its alternatives are randomly placed in 'sub... by ...' sequences of the chosen length. 22 | # 3. That can be repeated a few times, depending on how many lookups and lines per lookup you choose. 23 | # Input format: glyph and its alternatives space separated. Next glyph with its alternatives go to the next line 24 | """ 25 | 26 | GSSteppingTextField = objc.lookUpClass("GSSteppingTextField") 27 | 28 | 29 | class ArrowEditText (EditText): 30 | nsTextFieldClass = GSSteppingTextField 31 | 32 | def _setCallback(self, callback): 33 | super(ArrowEditText, self)._setCallback(callback) 34 | if callback is not None: # and self._continuous: 35 | self._nsObject.setContinuous_(True) 36 | self._nsObject.setAction_(self._target.action_) 37 | self._nsObject.setTarget_(self._target) 38 | 39 | 40 | class RandomFeature: 41 | 42 | def __init__(self): 43 | W, H = 400, 250 44 | Wmax, Hmax = 1000, 1000 45 | 46 | M = 10 47 | buttonWidth = 80 48 | inputWidth = buttonWidth * .6 49 | 50 | self.w = FloatingWindow((W, H), 'Random Feature', minSize=(W, H), maxSize=(Wmax, Hmax)) 51 | 52 | self.w.glyphs = TextEditor((M, M, -M, -M * 14), 'a a.ss01 a.ss02\nb b.ss01 b.ss02') 53 | 54 | # column 1 55 | self.w.featureTitle = TextBox((M, -M * 13, buttonWidth, M * 3), 'Feature') 56 | self.w.feature = EditText((M * 2 + buttonWidth, -M * 13.1, inputWidth, M * 2.5), 'calt') 57 | 58 | self.w.linesTitle = TextBox((M, -M * 10, buttonWidth, M * 3), 'Lines') 59 | self.w.lines = ArrowEditText((M * 2 + buttonWidth, -M * 10.1, inputWidth, M * 2.5), '20', continuous=False, callback=self.editTextCallback) 60 | 61 | self.w.sequenceTitle = TextBox((M, -M * 7, buttonWidth, M * 3), 'Sequence') 62 | self.w.sequence = ArrowEditText((M * 2 + buttonWidth, -M * 7.1, inputWidth, M * 2.5), '3', continuous=False, callback=self.editTextCallback) 63 | 64 | # column 2 65 | self.w.categoriesTitle = TextBox((M * 3 + buttonWidth * 1.7, -M * 13, buttonWidth, M * 3), 'Categories') 66 | self.w.categories = EditText((M * 3 + buttonWidth * 2.7, -M * 13.1, -M, M * 2.5), 'Letter Punctuation') 67 | 68 | self.w.lookupsTitle = TextBox((M * 3 + buttonWidth * 1.7, -M * 10, buttonWidth, M * 3), 'Lookups') 69 | self.w.lookups = ArrowEditText((M * 3 + buttonWidth * 2.7, -M * 10.1, inputWidth, M * 2.5), '4', continuous=False, callback=self.editTextCallback) 70 | 71 | self.w.classesTitle = TextBox((M * 3 + buttonWidth * 1.7, -M * 7, buttonWidth, M * 3), 'Classes') 72 | self.w.classes = ArrowEditText((M * 3 + buttonWidth * 2.7, -M * 7.1, inputWidth, M * 2.5), '5', continuous=False, callback=self.editTextCallback) 73 | 74 | self.w.runButton = Button((-M - buttonWidth, -M * 4, buttonWidth, M * 3), 'Run', callback=self.runCallback) 75 | 76 | self.w.open() 77 | 78 | def editTextCallback(self, sender): 79 | # int input only! 80 | inpt = sender.get() 81 | try: 82 | sender.set(str(int(inpt))) 83 | except: 84 | sender.set('3') 85 | 86 | def runCallback(self, sender): 87 | glyphs = {} 88 | 89 | # ---------- user input 90 | # get glyphs dict from user’s input, ignore missing glyphs 91 | inpt = self.w.glyphs.get() 92 | for line in inpt.split('\n'): 93 | alts = line.split() 94 | if alts: 95 | default = alts[0] 96 | if default: 97 | existingAlts = [] 98 | missingAlts = [] 99 | for i in range(len(alts) - 1): 100 | # check if requested alternatives are in the font 101 | for alt in alts: 102 | if alt in font.glyphs: 103 | if alt not in existingAlts: 104 | existingAlts.append(alt) 105 | else: 106 | missingAlts.append(alt) 107 | # add to the dict 108 | if existingAlts: 109 | glyphs[default] = existingAlts 110 | 111 | if missingAlts: 112 | # report missing alternatives 113 | print('Glyphs not found are ignored:') 114 | print(missingAlts) 115 | 116 | # other input 117 | try: 118 | featureName = self.w.feature.get() 119 | categories = self.w.categories.get().split() 120 | classesToAdd = int(self.w.classes.get()) 121 | linesToAdd = int(self.w.lines.get()) 122 | lookupsToAdd = int(self.w.lookups.get()) 123 | sequence = int(self.w.sequence.get()) 124 | except: 125 | print('Couldn’t read the input') 126 | return 127 | 128 | 129 | # ---------- classes 130 | # make classes 131 | classes = [] 132 | 133 | for i in range(classesToAdd): 134 | className = 'rand%s' % i 135 | classes.append(GSClass(className, '')) 136 | 137 | 138 | # add glyphs to the classes 139 | for glyph in font.glyphs: 140 | if glyph.category in categories and glyph.export: 141 | classIndex = randint(0, classesToAdd - 1) 142 | classes[classIndex].code += '%s ' % glyph.name 143 | 144 | # add classes to the font 145 | for clas in classes: 146 | classExists = False 147 | for clas2 in font.classes: 148 | if clas2.name == clas.name: 149 | clas2.code = clas.code 150 | classExists = True 151 | break 152 | if classExists is False: 153 | font.classes.append(clas) 154 | 155 | 156 | # ---------- feature 157 | # build code 158 | code = comment + '\n' 159 | 160 | for lookupIndex in range(lookupsToAdd): 161 | code += 'lookup random%s {\n' % lookupIndex 162 | 163 | for l in range(linesToAdd): 164 | line = '\tsub ' 165 | glyphFrom = choice(list(glyphs)) 166 | 167 | # avoid sub A by A 168 | for i in range(10): 169 | glyphTo = choice(glyphs[glyphFrom]) 170 | if glyphFrom != glyphTo: 171 | break 172 | 173 | targetGlyphIndex = randint(0, sequence - 1) 174 | for i in range(sequence): 175 | if i == targetGlyphIndex: 176 | line += '%s\' ' % glyphFrom 177 | else: 178 | line += '@rand%s ' % randint(0, classesToAdd - 1) 179 | line += 'by %s;' % glyphTo 180 | code += line + '\n' 181 | 182 | code += '} random%s;\n\n' % lookupIndex 183 | 184 | 185 | # add feature to the font 186 | featureExists = False 187 | for feature in font.features: 188 | if feature.name == featureName: 189 | feature.code = code 190 | featureExists = True 191 | break 192 | if featureExists is False: 193 | newFeature = GSFeature(featureName, code) 194 | font.features.append(newFeature) 195 | 196 | # compile features 197 | font.compileFeatures() 198 | 199 | 200 | RandomFeature() 201 | -------------------------------------------------------------------------------- /Font Info/Demo Instance Generator.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Demo Instances Generator 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Generates demo instances with limited character set and features from active instances. Accepts glyph names separated by spaces. 5 | """ 6 | import copy 7 | import vanilla 8 | from Foundation import NSUserDefaults 9 | from GlyphsApp import Glyphs, GSCustomParameter 10 | 11 | Glyphs.clearLog() 12 | 13 | thisFont = Glyphs.font 14 | 15 | m = 15 # margin 16 | tm = 35 # top/vertical margin 17 | bm = 50 # bottom margin 18 | 19 | 20 | class DemoFontsGenerator(object): 21 | 22 | def __init__(self): 23 | # Window 'self.w': 24 | windowWidth = 200 25 | windowHeight = 360 26 | windowWidthResize = 1200 # user can resize width by this value 27 | windowHeightResize = 500 # user can resize height by this value 28 | self.w = vanilla.FloatingWindow( 29 | (windowWidth, windowHeight), # default window size 30 | "Demo Instances Generator", # window title 31 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 32 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 33 | autosaveName="save.DemoInstancesGenerator.mainwindow" # stores last window position and size 34 | ) 35 | 36 | 37 | # UI elements: 38 | 39 | self.w.text_name = vanilla.TextBox((m, m - 3, windowWidth, tm), "Name (Suffix):", sizeStyle='small') 40 | self.w.text_glyphs = vanilla.TextBox((m, tm * 2 + m - 3, windowWidth, tm), "Limited Character Set:", sizeStyle='small') 41 | 42 | self.w.name = vanilla.TextEditor((m, tm, windowWidth - m * 2, tm), callback=self.SavePreferences, checksSpelling=False) 43 | self.w.glyphs = vanilla.TextEditor((m, tm * 3, windowWidth - m * 2, -bm), callback=self.SavePreferences, checksSpelling=False) 44 | 45 | self.windowResize(None) 46 | 47 | # Run Button: 48 | self.w.runButton = vanilla.Button((-80 - m, -20 - m, -m, m), "Generate", sizeStyle='regular', callback=self.generateDemoInstances) 49 | self.w.setDefaultButton(self.w.runButton) 50 | 51 | # Reset Button: 52 | self.w.resetButton = vanilla.Button(((-80 - m) * 2, -20 - m, (-80 - m) - m, m), "Reset", sizeStyle='regular', callback=self.ResetParameters) 53 | 54 | self.w.bind("resize", self.windowResize) 55 | 56 | # Load Settings: 57 | if not self.LoadPreferences(): 58 | print("Note: 'Demo Fonts Generator' could not load preferences. Will resort to defaults") 59 | 60 | # Open window and focus on it: 61 | self.w.open() 62 | self.w.makeKey() 63 | 64 | def windowResize(self, sender): 65 | windowWidth = self.w.getPosSize()[2] 66 | adaptiveWidth = windowWidth - m * 2 67 | 68 | self.w.name.setPosSize((m, tm, adaptiveWidth, tm)) 69 | self.w.glyphs.setPosSize((m, tm * 3, adaptiveWidth, -bm)) 70 | 71 | def SavePreferences(self, sender): 72 | try: 73 | Glyphs.defaults["save.DemoInstancesGenerator.name"] = self.w.name.get() 74 | Glyphs.defaults["save.DemoInstancesGenerator.glyphs"] = self.w.glyphs.get() 75 | except: 76 | return False 77 | return True 78 | 79 | def LoadPreferences(self): 80 | try: 81 | NSUserDefaults.standardUserDefaults().registerDefaults_({ 82 | "save.DemoInstancesGenerator.name": "Demo", 83 | "save.DemoInstancesGenerator.glyphs": "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z zero one two three four five seven eight period comma space hyphen b.calt f.calt h.calt k.calt l.calt f_t t_t A-cy Be-cy Ve-cy Ge-cy De-cy Ie-cy Io-cy Zhe-cy Ze-cy Ii-cy Iishort-cy Ka-cy El-cy Em-cy En-cy O-cy Pe-cy Er-cy Es-cy Te-cy U-cy Ef-cy Ha-cy Tse-cy Che-cy Sha-cy Shcha-cy Yeru-cy Softsign-cy Ereversed-cy Iu-cy Ia-cy a-cy be-cy ve-cy ge-cy de-cy ie-cy io-cy zhe-cy ze-cy ii-cy iishort-cy ka-cy el-cy em-cy en-cy o-cy pe-cy er-cy es-cy te-cy u-cy ef-cy ha-cy tse-cy che-cy sha-cy shcha-cy yeru-cy softsign-cy ereversed-cy iu-cy ia-cy .notdef ", 84 | }) 85 | self.w.name.set(Glyphs.defaults["save.DemoInstancesGenerator.name"]) 86 | self.w.glyphs.set(Glyphs.defaults["save.DemoInstancesGenerator.glyphs"]) 87 | except: 88 | return False 89 | 90 | return True 91 | 92 | def ResetParameters(self, sender): 93 | del Glyphs.defaults["save.DemoInstancesGenerator.name"] 94 | del Glyphs.defaults["save.DemoInstancesGenerator.glyphs"] 95 | self.w.name.set(Glyphs.defaults["save.DemoInstancesGenerator.name"]) 96 | self.w.glyphs.set(Glyphs.defaults["save.DemoInstancesGenerator.glyphs"]) 97 | 98 | def generateDemoInstances(self, sender): 99 | 100 | # pass glyph or class names here 101 | def replaceFeatureParameter(name): 102 | for fname, fcode in newFeatures.items(): 103 | if (" %s ") % (name) in fcode or (" %s;") % (name) in fcode or (" %s' ") % (name) in fcode: 104 | newFeatureCode = '' 105 | for line in fcode.splitlines(): 106 | if line.find(name) < 0: 107 | newFeatureCode = newFeatureCode + line + "\n" 108 | 109 | if len(newFeatureCode) == 0: 110 | if fname not in featuresToRemove: 111 | featuresToRemove.append(fname) 112 | newFeatures.pop(fname) 113 | else: 114 | newFeatures[fname] = newFeatureCode 115 | 116 | # glyphs and name to keep 117 | demoName = self.w.name.get() 118 | demoGlyphs = " %s " % (self.w.glyphs.get()) 119 | demoGlyphsList = list(demoGlyphs.split(" ")) 120 | 121 | # list glyphs to remove 122 | featuresToRemove = [] 123 | classesToRemove = [] 124 | replaceClasses = {} 125 | # replaceFeatures = {} 126 | 127 | newFeatures = {} 128 | for feature in thisFont.features: 129 | if feature.active == 1 and not feature.automatic: 130 | newFeatures[feature.name] = feature.code 131 | 132 | for glyph in thisFont.glyphs: 133 | if (" %s ") % (glyph.name) not in demoGlyphs: 134 | print('not in demo glyphs', glyph.name) 135 | # Replace Class custom parameter 136 | for GSClass in thisFont.classes: 137 | if GSClass.active == 1: 138 | if (" %s ") % glyph.name in (' %s ') % GSClass.code: 139 | print('----', glyph.name, GSClass.name) 140 | if GSClass.name not in replaceClasses.keys(): 141 | temp = (' %s ' % GSClass.code).replace(' %s ' % glyph.name, ' ') 142 | # if code is not empty, otherwise remove the class 143 | if temp.strip() != '': 144 | replaceClasses[GSClass.name] = temp.rstrip().lstrip() 145 | else: 146 | classesToRemove += GSClass.name 147 | replaceClasses.pop(GSClass.name) 148 | else: 149 | temp = (' %s ' % replaceClasses[GSClass.name]).replace(' %s ' % glyph.name, ' ') 150 | # if code is not empty, otherwise remove the class 151 | if temp.strip() != '': 152 | replaceClasses[GSClass.name] = temp.rstrip().lstrip() 153 | else: 154 | classesToRemove.append(GSClass.name) 155 | replaceClasses.pop(GSClass.name) 156 | 157 | # Replace Feature custom parameter (glyphs) 158 | replaceFeatureParameter(glyph.name) 159 | 160 | # Replace Feature custom parameter (empty classes) 161 | for GSClass in classesToRemove: 162 | replaceFeatureParameter('@%s' % GSClass) 163 | 164 | # remove features with no sub or pos 165 | for fname, fcode in newFeatures.items(): 166 | if 'sub' not in fcode and 'pos' not in fcode: 167 | featuresToRemove.append(fname) 168 | newFeatures.pop(fname) 169 | 170 | print('delete features: %s\n' % featuresToRemove) 171 | print('newFeatures: %s\n' % newFeatures) 172 | print('replaced Classes: %s\n' % replaceClasses) 173 | print('Remove Classes: %s\n' % classesToRemove) 174 | 175 | # Create copies of active instances, add limiting custom parameters, add Demo naming 176 | def copyInstances(): 177 | demoInstances = "" #list of demo instances 178 | 179 | for instance in thisFont.instances: 180 | if instance.active and (demoName in instance.name): 181 | demoInstances += "%s, " % instance.name 182 | 183 | for instance in thisFont.instances: 184 | if instance.active: 185 | # check if demo already exists 186 | if (("%s %s") % (instance.name, demoName) in demoInstances) or (demoName in instance.name): 187 | if demoName in instance.name: 188 | print("%s already exists" % instance.name) 189 | 190 | # if demo doesn't exist 191 | else: 192 | # copy active instances 193 | newInstance = copy.copy(instance) 194 | thisFont.instances.append(newInstance) 195 | 196 | # Demo preferredFamily (check if exists or use familyName) 197 | if newInstance.customParameters["preferredFamilyName"]: 198 | demoFamilyName = ("%s %s") % (newInstance.customParameters["preferredFamilyName"], demoName) 199 | newInstance.customParameters["preferredFamilyName"] = demoFamilyName 200 | #print(newInstance.customParameters) 201 | else: 202 | demoFamilyName = ("%s %s") % (thisFont.familyName, demoName) 203 | newInstance.customParameters["preferredFamilyName"] = demoFamilyName 204 | 205 | # Demo preferredSubfamily (check if exists or use instance name) 206 | if not newInstance.customParameters["preferredSubfamilyName"]: 207 | newInstance.customParameters["preferredSubfamilyName"] = newInstance.name 208 | 209 | 210 | # rename Demo instance (in Glyphs only) 211 | newInstance.name = ("%s %s") % (newInstance.name, demoName) 212 | #add it to the list 213 | demoInstances += "+ %s, " % newInstance.name 214 | 215 | 216 | # rename font files 217 | newInstance.customParameters["fileName"] = "%s - %s" % (newInstance.customParameters["preferredFamilyName"], newInstance.customParameters["preferredSubfamilyName"]) 218 | 219 | 220 | # limit glyphs 221 | newInstance.customParameters["Keep Glyphs"] = demoGlyphsList 222 | 223 | # limit classes 224 | for cname, ccode in replaceClasses.items(): 225 | newInstance.customParameters.append(GSCustomParameter("Replace Class", '%s; %s' % (cname, ccode))) 226 | if classesToRemove: 227 | newInstance.customParameters.append(GSCustomParameter("Remove Classes", classesToRemove)) 228 | 229 | # limit features 230 | for fname, fcode in newFeatures.items(): 231 | newInstance.customParameters.append(GSCustomParameter("Replace Feature", '%s; %s' % (fname, fcode))) 232 | if featuresToRemove: 233 | newInstance.customParameters.append(GSCustomParameter("Remove Features", featuresToRemove)) 234 | 235 | # update features parameter? 236 | #newInstance.customParameters["Update Features"] = True 237 | 238 | copyInstances() 239 | 240 | 241 | DemoFontsGenerator() 242 | -------------------------------------------------------------------------------- /Font Info/Preferred Names - Clean.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Clean Preferred Names 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes preferredFamilyName and preferredSubfamilyName custom parameters. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font 11 | cpName = ["preferredFamilyName", "preferredSubfamilyName"] 12 | 13 | for thisInstance in thisFont.instances: 14 | if not thisInstance.active: 15 | continue 16 | 17 | #print("Processing Instance:", thisInstance.name) 18 | for j in range(len(cpName)): 19 | for i in reversed(range(len(thisInstance.customParameters))): 20 | if thisInstance.customParameters[i].name == cpName[j]: 21 | del thisInstance.customParameters[i] 22 | print("Deleted from:", thisInstance.name) 23 | -------------------------------------------------------------------------------- /Font Info/Preferred Names - Set.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Set Preferred Names 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Copies familyName to preferredFamilyName, instance name to preferredSubfamilyName. 6 | Active instances only. 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | font = Glyphs.font 12 | 13 | 14 | for thisInstance in font.instances: 15 | 16 | if not thisInstance.active: 17 | continue 18 | 19 | print("Processing Instance:", thisInstance.name) 20 | preferredFamilyName = font.familyName 21 | 22 | if thisInstance.customParameters["preferredFamilyName"]: 23 | preferredFamilyName = thisInstance.customParameters["preferredFamilyName"] 24 | 25 | if thisInstance.customParameters["preferredSubfamilyName"]: 26 | preferredFamilyName = thisInstance.customParameters["preferredSubfamilyName"] 27 | 28 | thisInstance.customParameters["preferredFamilyName"] = preferredFamilyName 29 | thisInstance.customParameters["preferredSubfamilyName"] = thisInstance.name 30 | #print(" preferredFamilyName:", preferredFamilyName) 31 | #print(" preferredSubfamilyName:", preferredStyleName) 32 | -------------------------------------------------------------------------------- /Font Info/Set Vertical Metrics.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Set Vertical Metrics 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | from vanilla import FloatingWindow, List, TextBox, EditText, Button, CheckBox, Slider 5 | from AppKit import NSApp, NSBezierPath, NSRect, NSColor 6 | import math 7 | import traceback # print(traceback.format_exc()) 8 | 9 | __doc__=""" 10 | Set vertical metrics. 11 | """ 12 | 13 | # NOTES 14 | # This version is based on Google guide: https://github.com/googlefonts/gf-docs/blob/main/VerticalMetrics/README.md 15 | 16 | Glyphs.clearLog() 17 | 18 | def nicely_round(value): 19 | if int(value) == value: 20 | return int(value) 21 | return round(value, 1) 22 | 23 | def round_up_to_multiple(number, multiple): 24 | return multiple * math.ceil(number / multiple) 25 | 26 | def remap(oValue, oMin, oMax, nMin, nMax): 27 | oRange = (oMax - oMin) 28 | if oRange == 0: 29 | nValue = nMin 30 | else: 31 | nRange = (nMax - nMin) 32 | nValue = (((oValue - oMin) * nRange) / oRange) + nMin 33 | return nValue 34 | 35 | 36 | GSSteppingTextField = objc.lookUpClass("GSSteppingTextField") 37 | class ArrowEditText(EditText): 38 | nsTextFieldClass = GSSteppingTextField 39 | def _setCallback(self, callback): 40 | objc.super(ArrowEditText, self)._setCallback(callback) 41 | if callback is not None: 42 | self._nsObject.setContinuous_(True) 43 | self._nsObject.setAction_(self._target.action_) 44 | self._nsObject.setTarget_(self._target) 45 | 46 | class SetVerticalMetrics(): 47 | def __init__(self): 48 | self.close_previous_windows() 49 | 50 | self.font = Glyphs.font 51 | if not self.font: 52 | print('No font is open') 53 | return 54 | 55 | self.selected_masters = [] 56 | self.all_highest, self.all_lowest = [], [] 57 | self.ignore_non_exporting_glyphs = True 58 | self.round = 10 # round all metric values to multiple of this 59 | 60 | self.win_metrics = [None, None] 61 | self.typo_metrics = [None, None, None] 62 | self.hhea_metrics = [None, None, None] 63 | 64 | self.show_metrics = [] # checkboxes: 1 win 2 typo 3 hhea 65 | self.colors = [(1, 0, 0, 1), # win = red 66 | (0, 0, 1, .1), # typo = blue 67 | (0, 1, 0, .1)] # hhea = green 68 | 69 | W, H = 400, 500 70 | M = 10 71 | column = W/4 72 | self.w = FloatingWindow((W, H), 73 | minSize = (400, 255), 74 | maxSize = (400, 2000), 75 | title = 'Set Vertical Metrics — ' + self.font.familyName, 76 | autosaveName = 'com.slobzheninov.SetVerticalMetrics.mainwindow') 77 | 78 | # show checkboxes 79 | y = M 80 | self.w.show_title = TextBox((M, y, column, M*2), 'Show') 81 | self.w.show_win = CheckBox((column, y-1, column/2, M*2), 'Win', callback = self.show_callback) 82 | self.w.show_win.ID = 0 83 | self.w.show_typo = CheckBox((column*2, y-1, column/2, M*2), 'Typo', callback = self.show_callback) 84 | self.w.show_typo.ID = 1 85 | self.w.show_hhea = CheckBox((column*3, y-1, column/2, M*2), 'Hhea', callback = self.show_callback) 86 | self.w.show_hhea.ID = 2 87 | 88 | # metric titles 89 | mlt = 3 90 | ascender_y = y + M * mlt 91 | descender_y = ascender_y + M * mlt 92 | gap_y = descender_y + M * mlt 93 | 94 | self.w.ascender_title = TextBox((M, ascender_y, column, M*2), 'Ascender') 95 | self.w.descender_title = TextBox((M, descender_y, column, M*2), 'Descender') 96 | self.w.linegap_title = TextBox((M, gap_y, column, M*2), 'Line Gap') 97 | 98 | # ascender editors 99 | self.w.win_ascender = ArrowEditText((column, ascender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback) 100 | self.w.typo_ascender = ArrowEditText((column*2, ascender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback) 101 | self.w.hhea_ascender = ArrowEditText((column*3, ascender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback, placeholder = 'auto', ) 102 | self.w.win_ascender.ID = 'win_ascender' 103 | self.w.typo_ascender.ID = 'typo_ascender' 104 | self.w.hhea_ascender.ID = 'hhea_ascender' 105 | 106 | # descender editors 107 | self.w.win_descender = ArrowEditText((column, descender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback) 108 | self.w.typo_descender = ArrowEditText((column*2, descender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback) 109 | self.w.hhea_descender = ArrowEditText((column*3, descender_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback, placeholder = 'auto', ) 110 | self.w.win_descender.ID = 'win_descender' 111 | self.w.typo_descender.ID = 'typo_descender' 112 | self.w.hhea_descender.ID = 'hhea_descender' 113 | 114 | # line gap editors 115 | self.w.typo_linegap = ArrowEditText((column*2, gap_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback) 116 | self.w.hhea_linegap = ArrowEditText((column*3, gap_y, column-M, M*2), '', continuous = False, callback = self.metrics_callback, placeholder = 'auto') 117 | self.w.typo_linegap.ID = 'typo_linegap' 118 | self.w.hhea_linegap.ID = 'hhea_linegap' 119 | 120 | # line height and centering 121 | y = gap_y + M * mlt 122 | self.w.line_height_title = TextBox((M, y, column-M, M*2), 'Line Height') 123 | self.w.line_height = EditText((column, y, column/2.2, M*2), '', placeholder = '1.2') 124 | try: 125 | self.w.line_height.setToolTip('Defines the default line height, esp on the web.\nThe recommended value is about 1.2 because that’s the default in many apps') 126 | except: 127 | pass # Glyphs 2 128 | 129 | self.w.centering_title = TextBox((column*2, y, column-M, M*2), 'Center: n') 130 | # self.w.centering = EditText((column*2.2, y, column/2.2, M*2), '1', placeholder = '0–1') 131 | self.w.centering_H = TextBox((-M*2.3, y, -M, M*2), 'H') 132 | self.w.centering = Slider((column*2.7, y, -M*3, M*2), value = 1, minValue = 0, maxValue = 1, tickMarkCount = 7, stopOnTickMarks = True, sizeStyle = 'mini') 133 | 134 | # checkboxes 135 | y = y + M * mlt 136 | self.w.use_typo = CheckBox((column*2, y, column*2, M*2), 'Use Typo Metrics (recommended)', sizeStyle = 'small', value = True) 137 | self.w.use_in_Glyphs = CheckBox((M+2, y, column*2, M*2), 'Use in Glyphs', sizeStyle = 'small', value = True) 138 | 139 | # masters table 140 | y = y + M * mlt 141 | self.w.masters = List((M, y, -M, -M*5), 142 | [{'Masters': master.name} for master in self.font.masters], 143 | columnDescriptions = [{'title': 'Masters', 'editable': False}, 144 | {'title': 'Highest', 'editable': False}, 145 | {'title': 'Lowest', 'editable': False}], 146 | doubleClickCallback = self.list_double_click, 147 | allowsSorting = False, 148 | allowsEmptySelection = False) 149 | self.w.masters.setSelection([i for i in range(len(self.font.masters))]) # select all by default 150 | 151 | # buttons 152 | self.w.calc = Button((M, -M*4, column-M*2, M*2), 'Calculate', callback = self.calc) 153 | self.w.current = Button((column, -M*4, column-M*2, M*2), 'Current', callback = self.get_current) 154 | self.w.apply = Button((column*3, -M*4, column-M, M*2), 'Apply', callback = self.apply) 155 | 156 | self.add_callbacks() 157 | self.w.bind('close', self.remove_callbacks) 158 | self.w.open() 159 | 160 | self.get_current() 161 | 162 | 163 | def list_double_click(self, sender): 164 | if not sender.get(): 165 | return 166 | 167 | new_layers = [] 168 | for i in sender.getSelection(): 169 | try: 170 | value = sender.get()[i] 171 | highest_glyph = self.font.glyphs[value['Highest'].split()[1]] 172 | highest_layer = highest_glyph.layers[self.font.masters[i].id] 173 | 174 | lowest_glyph = self.font.glyphs[value['Lowest'].split()[1]] 175 | lowest_layer = lowest_glyph.layers[self.font.masters[i].id] 176 | new_layers.extend([highest_layer, lowest_layer]) 177 | except: 178 | print(traceback.format_exc()) 179 | if new_layers: 180 | tab = self.font.currentTab if self.font.currentTab else self.font.newTab() 181 | new_tab_layers = list(tab.layers) 182 | # new_tab_layers.insert(tab.layersCursor, new_layers) 183 | new_tab_layers[tab.layersCursor:tab.layersCursor] = new_layers 184 | tab.layers = new_tab_layers 185 | 186 | 187 | def close_previous_windows(self): 188 | for window in NSApp.windows(): 189 | if 'Set Vertical Metrics' in window.title(): 190 | window.close() 191 | 192 | def add_callbacks(self): 193 | Glyphs.addCallback(self.draw_background, DRAWBACKGROUND) 194 | 195 | def remove_callbacks(self, sender): 196 | Glyphs.removeCallback(self.draw_background, DRAWBACKGROUND) 197 | 198 | def draw_background(self, current_layer, event): 199 | if not self.show_metrics: 200 | return 201 | 202 | for i in self.show_metrics: 203 | metric = [self.win_metrics, self.typo_metrics, self.hhea_metrics][i] 204 | 205 | if None in metric[:1]: 206 | continue 207 | 208 | self.rect(x = -100000, 209 | y = metric[1] if i != 0 else -metric[1], 210 | w = current_layer.width + 100000 * 2, 211 | h = metric[0] - metric[1] if i != 0 else metric[0] + metric[1], 212 | color = self.colors[i], 213 | stroke = None if i != 0 else 5) # stroke for win, and fill for typo and hhea 214 | 215 | def rect(self, x, y, w, h, color, stroke = None): 216 | # make path 217 | rect = NSRect((x, y), (w, h)) 218 | bezierPath = NSBezierPath.alloc().init() 219 | bezierPath.appendBezierPathWithRect_(rect) 220 | r, g, b, alpha = color 221 | NSColor.colorWithCalibratedRed_green_blue_alpha_(r, g, b, alpha).set() 222 | if stroke: 223 | bezierPath.setLineWidth_(stroke) 224 | bezierPath.stroke() 225 | else: # fill 226 | bezierPath.fill() 227 | 228 | def show_callback(self, sender): 229 | if sender.get(): 230 | if sender.ID not in self.show_metrics: 231 | self.show_metrics.append(sender.ID) 232 | else: 233 | if sender.ID in self.show_metrics: 234 | self.show_metrics.remove(sender.ID) 235 | 236 | def metrics_callback(self, sender = None): 237 | # if no sender, call this callback for all the buttons 238 | if sender is None: 239 | for edittext in [self.w.win_ascender, self.w.win_descender, self.w.typo_ascender, self.w.typo_descender, self.w.typo_linegap, self.w.hhea_ascender, self.w.hhea_descender, self.w.hhea_linegap]: 240 | self.metrics_callback(edittext) 241 | return 242 | 243 | if sender.get() != '': 244 | try: 245 | value = int(sender.get()) 246 | except: 247 | value = 'fail' 248 | else: 249 | value = None 250 | 251 | kind, metric = sender.ID.split('_') 252 | i = ['ascender', 'descender', 'linegap'].index(metric) 253 | 254 | if kind == 'win': 255 | if value == 'fail': 256 | sender.set(self.win_metrics[i]) 257 | else: 258 | self.win_metrics[i] = value 259 | elif kind == 'typo': 260 | if value == 'fail': 261 | sender.set(self.typo_metrics[i]) 262 | else: 263 | self.typo_metrics[i] = value 264 | elif kind == 'hhea': 265 | if value == 'fail': 266 | sender.set(self.hhea_metrics[i]) 267 | else: 268 | self.hhea_metrics[i] = value 269 | 270 | def get_highest_and_lowest(self, font): 271 | all_highest, all_lowest = [], [] 272 | for i, master in enumerate(font.masters): 273 | masterID = master.id 274 | glyphs_bottoms_and_tops = [] 275 | for glyph in font.glyphs: 276 | if glyph.export or not self.ignore_non_exporting_glyphs: 277 | glyphs_bottoms_and_tops.append([glyph.name, glyph.layers[masterID].bounds.origin.y, glyph.layers[masterID].bounds.origin.y + glyph.layers[masterID].bounds.size.height]) 278 | highest = sorted(glyphs_bottoms_and_tops, key=lambda x: -x[2])[0] # ['Aring', 0.0, 899.0] 279 | lowest = sorted(glyphs_bottoms_and_tops, key=lambda x: x[1])[0] # ['at', -249.0, 749.0] 280 | highest = nicely_round(highest[2]), highest[0] 281 | lowest = nicely_round(lowest[1]), lowest[0] 282 | all_highest.append(highest) 283 | all_lowest.append(lowest) 284 | 285 | # set to UI 286 | self.w.masters[i] = {'Highest': str(highest[0]) + ' ' + highest[1], 287 | 'Lowest': str(lowest[0]) + ' ' + lowest[1]} 288 | return all_highest, all_lowest 289 | 290 | def get_highest_and_lowest_for_masters(self, indexes): 291 | selected_highest, selected_lowest = [], [] 292 | for i in indexes: 293 | selected_highest.append(self.all_highest[i]) 294 | selected_lowest.append(self.all_lowest[i]) 295 | sel_highest = sorted(selected_highest, key=lambda x: -x[0])[0] 296 | sel_lowest = sorted(selected_lowest, key=lambda x: x[0])[0] 297 | return sel_highest, sel_lowest 298 | 299 | def get_highest_Aacute(self, font, indexes): 300 | if 'Aacute' not in font.glyphs: 301 | return None, None 302 | glyph = font.glyphs['Aacute'] 303 | tops = [[glyph.layers[font.masters[i].id].bounds.origin.y + glyph.layers[font.masters[i].id].bounds.size.height, i] for i in indexes] 304 | highest = sorted(tops, key=lambda x: -x[0])[0] 305 | return highest # this is [y, i] 306 | 307 | def calc(self, sender = None): 308 | if not self.font: 309 | print('Font not found: ' + self.w.getTitle()) 310 | return 311 | # self.font = Glyphs.font 312 | # if not self.font: 313 | # return 314 | # self.w.title = 'Set Vertical Metrics — ' + self.font.familyName 315 | # get current selection if any 316 | 317 | current_selection = self.w.masters.getSelection() 318 | self.w.masters.set([{'Masters': master.name} for master in self.font.masters]) 319 | self.w.masters.setSelection(current_selection if current_selection else [i for i in range(len(self.font.masters))]) # set last selection or all by default 320 | 321 | self.all_highest, self.all_lowest = self.get_highest_and_lowest(self.font) 322 | selected_masters_indexes = self.w.masters.getSelection() 323 | 324 | # get lowest and highest for the selection 325 | sel_highest, sel_lowest = self.get_highest_and_lowest_for_masters(selected_masters_indexes) 326 | 327 | # ----- set WIN 328 | # simply set to lowest and highest glyphs 329 | self.w.win_ascender.set(round_up_to_multiple(sel_highest[0], self.round)) 330 | self.w.win_descender.set(round_up_to_multiple(abs(sel_lowest[0]), self.round)) # absolute 331 | 332 | # ----- set TYPO 333 | # The sum of the font’s vertical metric values (absolute) should be 20-30% greater than the font’s UPM: 334 | # https://github.com/googlefonts/gf-docs/blob/main/VerticalMetrics/README.md#11-the-sum-of-the-fonts-vertical-metric-values-absolute-should-be-20-30-greater-than-the-fonts-upm 335 | # try: 336 | # centering = float(self.w.centering.get()) 337 | # if not 0 < centering < 1: 338 | # to_except # this should skip to except 339 | # except: 340 | # centering = 1 # 0 = center to xHeight, 1 = center to capHeight 341 | # self.w.centering.set(centering) 342 | try: 343 | line_height = float(self.w.line_height.get()) 344 | except: 345 | line_height = None 346 | 347 | 348 | # use Aacute (WIP: needs a fallback alternative) 349 | highest_Aacute, master_index = self.get_highest_Aacute(self.font, selected_masters_indexes) 350 | if highest_Aacute: 351 | master = self.font.masters[master_index] 352 | minimal_ascender = round_up_to_multiple(highest_Aacute, self.round) 353 | center = remap(self.w.centering.get(), 0, 1, master.xHeight/2, master.capHeight/2) 354 | dist = minimal_ascender - center 355 | minimal_descender = center - dist 356 | 357 | # ----- apply line height 358 | current_line_height_abs = minimal_ascender - minimal_descender 359 | current_line_height = round(current_line_height_abs / self.font.upm, 2) 360 | 361 | # no line_height input, use Aacute 362 | if line_height is None: 363 | self.w.line_height.set(str(current_line_height)) 364 | 365 | ascender = minimal_ascender 366 | descender = minimal_descender 367 | 368 | # add leading to both ascender and descender 369 | else: 370 | diff = self.font.upm * line_height - current_line_height_abs 371 | diff = diff + 1 if diff % 2 != 0 else diff # make sure the difference is even 372 | ascender = round_up_to_multiple(minimal_ascender + diff/2, self.round) 373 | descender = -round_up_to_multiple(abs(minimal_descender - diff/2), self.round) 374 | 375 | # if shorter than Aacute, notify 376 | if current_line_height > line_height: 377 | Glyphs.showMacroWindow() 378 | print('Line height measured by Aacute is ', current_line_height, ', your input is shorter: ', line_height) 379 | 380 | self.w.typo_ascender.set(ascender) 381 | self.w.typo_descender.set(descender) 382 | self.w.typo_linegap.set('0') 383 | 384 | # ----- set HHEA to auto 385 | self.w.hhea_ascender.set('') 386 | self.w.hhea_descender.set('') 387 | self.w.hhea_linegap.set('') 388 | 389 | # run the callbacks for all the buttons 390 | self.metrics_callback() 391 | 392 | 393 | def get_current(self, sender = None): 394 | # self.font = Glyphs.font 395 | if not self.font: 396 | print('Font not found: ' + self.w.getTitle()) 397 | return 398 | for parameter, textbox in { 399 | 'typoAscender': self.w.typo_ascender, 400 | 'typoDescender': self.w.typo_descender, 401 | 'typoLineGap': self.w.typo_linegap, 402 | 'hheaAscender': self.w.hhea_ascender, 403 | 'hheaDescender': self.w.hhea_descender, 404 | 'hheaLineGap': self.w.hhea_linegap, 405 | 'winAscent': self.w.win_ascender, 406 | 'winDescent': self.w.win_descender, 407 | }.items(): 408 | value = self.font.selectedFontMaster.customParameters[parameter] 409 | textbox.set(value) 410 | # set line height if empty 411 | try: 412 | if not self.w.line_height.get(): 413 | current_line_height_abs = self.w.typo_ascender.get() - self.w.typo_descender.get() 414 | self.w.line_height.set(round(current_line_height_abs / self.font.upm, 2)) 415 | except: 416 | pass 417 | # run the callbacks for all the buttons 418 | self.metrics_callback() 419 | 420 | 421 | def apply(self, sender=None): 422 | # apply to the current font 423 | # self.font = Glyphs.font 424 | if not self.font: 425 | print('Font not found: ' + self.w.getTitle()) 426 | return 427 | 428 | for value in self.win_metrics + self.typo_metrics: 429 | if value is None: 430 | print('The script expects all WIN and TYPO parameters to work') 431 | return 432 | 433 | selected_masters_indexes = self.w.masters.getSelection() 434 | for i in selected_masters_indexes: 435 | master = self.font.masters[i] 436 | 437 | master.customParameters['winAscent'] = self.win_metrics[0] 438 | master.customParameters['winDescent'] = self.win_metrics[1] 439 | 440 | master.customParameters['typoAscender'] = self.typo_metrics[0] 441 | master.customParameters['typoDescender'] = self.typo_metrics[1] 442 | master.customParameters['typoLineGap'] = self.typo_metrics[2] 443 | 444 | master.customParameters['hheaAscender'] = self.hhea_metrics[0] if self.hhea_metrics[0] is not None else self.typo_metrics[0] 445 | master.customParameters['hheaDescender'] = self.hhea_metrics[1] if self.hhea_metrics[1] is not None else self.typo_metrics[1] 446 | master.customParameters['hheaLineGap'] = self.hhea_metrics[2] if self.hhea_metrics[2] is not None else self.typo_metrics[2] 447 | 448 | # set use typo metrics 449 | self.font.customParameters['Use Typo Metrics'] = self.w.use_typo.get() 450 | 451 | # set custom parameter (use in Glyphs) 452 | if self.w.use_in_Glyphs.get(): 453 | self.font.customParameters['EditView Line Height'] = self.typo_metrics[0] - self.typo_metrics[1] + self.typo_metrics[2] 454 | 455 | 456 | 457 | 458 | SetVerticalMetrics() -------------------------------------------------------------------------------- /Font Info/glyphOrder - Paste.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: glyphOrder - Paste 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | from Foundation import NSString, NSPasteboard, NSUTF8StringEncoding 5 | __doc__ = """ 6 | Pastes copied glyph names to selected index in glyphOrder. 7 | 1. Copy glyph names (space separated) 8 | 2. Select glyph, in front of which you want to paste them 9 | 3. Run this script 10 | If glyphOrder custom parameter is missing, it will be created. 11 | """ 12 | 13 | from GlyphsApp import Glyphs 14 | 15 | Glyphs.clearLog() 16 | font = Glyphs.font 17 | 18 | 19 | def getGlyphNamesFromPasteboard(): 20 | # get glyph names from pasteboard 21 | pasteboard = NSPasteboard.generalPasteboard() 22 | typeName = pasteboard.availableTypeFromArray_(["public.utf8-plain-text"]) 23 | data = pasteboard.dataForType_(typeName) 24 | text = NSString.alloc().initWithData_encoding_(data, NSUTF8StringEncoding) 25 | copiedGlyphs = text.split() 26 | return copiedGlyphs 27 | 28 | 29 | def filterCopiedGlyphs(copiedGlyphs): 30 | glyphs = [] 31 | for glyph in copiedGlyphs: 32 | if glyph in font.glyphs: 33 | glyphs.append(glyph) 34 | return glyphs 35 | 36 | 37 | def getGlyphOrder(): 38 | # create if missing 39 | if 'glyphOrder' not in font.customParameters: 40 | glyphOrder = [] 41 | for glyph in font.glyphs: 42 | glyphOrder.append(glyph.name) 43 | else: 44 | glyphOrder = list(font.customParameters['glyphOrder']) 45 | return glyphOrder 46 | 47 | 48 | def removeFromGlyphsOrder(glyphOrder, copiedGlyphs): 49 | for glyph in copiedGlyphs: 50 | if glyph in glyphOrder: 51 | glyphOrder.remove(glyph) 52 | return glyphOrder 53 | 54 | 55 | def getSelectedIndex(glyphOrder): 56 | if font.selectedLayers: 57 | selectedGlyph = font.selectedLayers[0].parent.name 58 | selectedIndex = glyphOrder.index(selectedGlyph) 59 | return selectedIndex 60 | 61 | 62 | def pasteToIndex(copiedGlyphs, index, glyphOrder): 63 | glyphOrder[index:index] = copiedGlyphs 64 | return glyphOrder 65 | 66 | 67 | def setCustomParameter(glyphOrder): 68 | font.customParameters['glyphOrder'] = glyphOrder 69 | 70 | 71 | # run 72 | copiedGlyphs = getGlyphNamesFromPasteboard() 73 | copiedGlyphs = filterCopiedGlyphs(copiedGlyphs) 74 | glyphOrder = getGlyphOrder() 75 | glyphOrder = removeFromGlyphsOrder(glyphOrder, copiedGlyphs) 76 | selectedIndex = getSelectedIndex(glyphOrder) 77 | glyphOrder = pasteToIndex(copiedGlyphs, selectedIndex, glyphOrder) 78 | setCustomParameter(glyphOrder) 79 | -------------------------------------------------------------------------------- /G2 Harmonize.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: G2 Harmonize 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | from math import sqrt 5 | from GlyphsApp import Glyphs 6 | from Cocoa import NSPoint 7 | 8 | # https://gist.github.com/simoncozens/3c5d304ae2c14894393c6284df91be5b source 9 | 10 | 11 | Glyphs.clearLog() 12 | 13 | font = Glyphs.font 14 | layer = font.selectedLayers[0] 15 | 16 | 17 | def getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): 18 | px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)) 19 | py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)) 20 | return (px, py) 21 | 22 | 23 | def getDist(a, b): 24 | dist = sqrt((b.x - a.x)**2 + (b.y - a.y)**2) 25 | return dist 26 | 27 | 28 | def remap(oldValue, oldMin, oldMax, newMin, newMax): 29 | try: 30 | oldRange = (oldMax - oldMin) 31 | newRange = (newMax - newMin) 32 | newValue = (((oldValue - oldMin) * newRange) / oldRange) + newMin 33 | return newValue 34 | except: 35 | pass 36 | 37 | 38 | if layer.selection: 39 | for node in layer.selection: 40 | if node.smooth and node.nextNode.type == 'offcurve' and node.prevNode.type == 'offcurve': 41 | P = node 42 | 43 | # find intersection of lines created by offcurves 44 | intersection = (getIntersection(P.nextNode.x, P.nextNode.y, P.nextNode.nextNode.x, P.nextNode.nextNode.y, P.prevNode.x, P.prevNode.y, P.prevNode.prevNode.x, P.prevNode.prevNode.y)) 45 | d = NSPoint(intersection[0], intersection[1]) 46 | 47 | # find ratios 48 | p0 = getDist(P.nextNode.nextNode, P.nextNode) / getDist(P.nextNode, d) 49 | p1 = getDist(d, P.prevNode) / getDist(P.prevNode, P.prevNode.prevNode) 50 | # ratio 51 | p = sqrt(p0 * p1) 52 | 53 | # set onpoint position based on that p ratio 54 | t = p / (p + 1) 55 | 56 | # oncurve 57 | P.x = remap(t, 0, 1, P.nextNode.x, P.prevNode.x) 58 | P.y = remap(t, 0, 1, P.nextNode.y, P.prevNode.y) 59 | 60 | # handles 61 | 62 | # deltaX = P.x - remap(t, 0, 1, P.nextNode.x, P.prevNode.x) 63 | # deltaY = P.y - remap(t, 0, 1, P.nextNode.y, P.prevNode.y) 64 | 65 | # P.nextNode.x += deltaX 66 | # P.nextNode.y += deltaY 67 | # P.prevNode.x += deltaX 68 | # P.prevNode.y += deltaY 69 | 70 | layer.updateMetrics() 71 | -------------------------------------------------------------------------------- /Glyphs/Copy Missing Special Layers.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Missing Special Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Copies special layers from the 1st selected glyph to other selected glyphs (in a tab). 6 | Only copies layers that are missing in the target glyphs. 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | font = Glyphs.font 11 | 12 | Glyphs.clearLog() 13 | 14 | if len(font.selectedLayers) > 1: 15 | sourceGlyph = font.selectedLayers[0].parent 16 | 17 | for selectedLayer in font.selectedLayers[1:]: 18 | targetGlyph = selectedLayer.parent 19 | 20 | for layer in sourceGlyph.layers: 21 | if layer.isSpecialLayer: 22 | 23 | # check if layer exists in target layer 24 | layerFound = False 25 | for layer2 in targetGlyph.layers: 26 | if layer2.name == layer.name: 27 | layerFound = True 28 | 29 | if layerFound is False: 30 | newLayer = layer.copy() 31 | targetGlyph.layers.append(newLayer) 32 | newLayer.reinterpolate() 33 | print('Copied layer: %s' % layer.name) 34 | -------------------------------------------------------------------------------- /Glyphs/Reorder Shapes.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Reorder Shapes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Reorders paths by their length, y and x. Reorders components by glyph name, y and x. 6 | """ 7 | # sort paths by length, y, x 8 | # sort components by glyph name, y, x 9 | # round coordinates to 10, since anything smaller is probably an error 10 | 11 | from GlyphsApp import Glyphs 12 | font = Glyphs.font 13 | 14 | 15 | for selectedLayer in font.selectedLayers: 16 | glyph = selectedLayer.parent 17 | for layer in glyph.layers: 18 | 19 | # glyphs 2 20 | if Glyphs.versionNumber < 3: 21 | layer.paths = sorted(layer.paths, key=lambda path: (len(path), round(path.bounds.origin.y, -1), round(path.bounds.origin.x, -1))) 22 | layer.components = sorted(layer.components, key=lambda component: (component.name, round(component.bounds.origin.y, -1), round(component.bounds.origin.x, -1))) 23 | 24 | # glyphs 3+ 25 | else: 26 | # sort components 27 | components = sorted(layer.components, key=lambda component: (component.glyph.name, round(component.bounds.origin.y, -1), round(component.bounds.origin.x, -1))) 28 | for i, component in enumerate(reversed(components)): 29 | index = layer.shapes.index(component) 30 | layer.shapes.pop(index) 31 | layer.shapes.insert(0, component) 32 | 33 | # sort paths 34 | paths = sorted(layer.paths, key=lambda path: (len(path), round(path.bounds.origin.y, -1), round(path.bounds.origin.x, -1))) 35 | for i, path in enumerate(reversed(paths)): 36 | index = layer.shapes.index(path) 37 | layer.shapes.pop(index) 38 | layer.shapes.insert(0, path) 39 | -------------------------------------------------------------------------------- /Glyphs/Show All Layers.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Show All Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Show all layers of the selected glyphs in a new tab 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSControlLayer 9 | 10 | layers = Glyphs.font.selectedLayers 11 | allLayers = [] 12 | 13 | # get all layers 14 | for layer in layers: 15 | if not isinstance(layer, GSControlLayer): 16 | if layer.parent: 17 | for l in layer.parent.layers: 18 | allLayers.append(l) 19 | allLayers.append(GSControlLayer(10)) # newline 20 | 21 | # append to a new tab 22 | if allLayers: 23 | tab = Glyphs.font.newTab('') 24 | for layer in allLayers: 25 | tab.layers.append(layer) 26 | -------------------------------------------------------------------------------- /Kerning/Delete Kerning Pair From All Masters.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Delete Kerning Pair From All Masters 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes kerning for the selected pair(s) from all masters. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, LTR 9 | font = Glyphs.font 10 | 11 | 12 | def removeKerning(leftLayer, rightLayer): 13 | # if both are master layers and the master is the same 14 | if leftLayer.isMasterLayer and rightLayer.isMasterLayer and leftLayer.master == rightLayer.master: 15 | # master = leftLayer.master 16 | leftKey = leftLayer.parent.rightKerningKey 17 | rightKey = rightLayer.parent.leftKerningKey 18 | 19 | # set kerning 20 | for master in font.masters: 21 | font.removeKerningForPair(master.id, leftKey, rightKey, direction=LTR) 22 | 23 | 24 | # more than 2 layers selected 25 | if len(font.selectedLayers) > 2: 26 | for i, layer in enumerate(font.selectedLayers): 27 | 28 | # unless it's the last layer 29 | if i < len(font.selectedLayers) - 1: 30 | leftLayer = layer 31 | rightLayer = font.selectedLayers[i + 1] 32 | 33 | removeKerning(leftLayer, rightLayer) 34 | 35 | 36 | # no selection (current pair) 37 | else: 38 | # get current pair 39 | tab = font.currentTab 40 | cursor = tab.textCursor 41 | if Glyphs.versionNumber >= 3: # Glyphs 3 42 | cachedLayers = tab.composedLayers 43 | else: # Glyphs 2 44 | cachedLayers = tab.graphicView().layoutManager().cachedGlyphs() 45 | 46 | # if at least two layers in the edit view 47 | if cachedLayers and len(cachedLayers) > 1: 48 | leftLayer = cachedLayers[cursor - 1] 49 | rightLayer = cachedLayers[cursor] 50 | 51 | removeKerning(leftLayer, rightLayer) 52 | -------------------------------------------------------------------------------- /Kerning/Kern to max.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Kern to max 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | For the selected pair, sets the kerning value to maximum. Maximum is the half width of the narrower glyph in the pair. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | # set kerning to the maximum (half of the narrower glyph in the pair) 11 | 12 | font = Glyphs.font 13 | 14 | tab = font.currentTab 15 | # get current pair 16 | cursor = tab.textCursor 17 | 18 | 19 | if Glyphs.versionNumber >= 3: # Glyphs 3 20 | cachedLayers = tab.composedLayers 21 | else: # Glyphs 2 22 | cachedLayers = tab.graphicView().layoutManager().cachedGlyphs() 23 | 24 | # if at least two layers in the edit view 25 | if cachedLayers and len(cachedLayers) > 1: 26 | leftLayer = cachedLayers[cursor - 1] 27 | rightLayer = cachedLayers[cursor] 28 | 29 | # if both are master layers and the master is the same 30 | if leftLayer.isMasterLayer and rightLayer.isMasterLayer and leftLayer.master == rightLayer.master: 31 | master = leftLayer.master 32 | leftKey = leftLayer.parent.rightKerningKey 33 | rightKey = rightLayer.parent.leftKerningKey 34 | 35 | # kerning value is half width of the narrower layer in the pair 36 | if leftLayer.width < rightLayer.width: 37 | kerningValue = - int(leftLayer.width / 2) 38 | else: 39 | kerningValue = - int(rightLayer.width / 2) 40 | 41 | # set kerning 42 | font.setKerningForPair(master.id, leftKey, rightKey, kerningValue) 43 | -------------------------------------------------------------------------------- /Kerning/Steal Kerning From Next Pair at Cursor's Y.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Steal kerning from next pair at cursor's Y 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | from AppKit import NSPoint 5 | __doc__ = """ 6 | Copies kerning value from the next pair, as measured at the current cursor’s vertical position. 7 | Example: to kern AV the same as VA, type AVVA, place cursor between AV, and point with the mouse where vertically you want to measure the distance. 8 | """ 9 | Glyphs.clearLog() 10 | font = Glyphs.font 11 | 12 | def getLayers(tab): 13 | try: 14 | textCursor = tab.textCursor 15 | if textCursor == 0: 16 | return False 17 | layerA = tab.layers[textCursor-1] 18 | layerB = tab.layers[textCursor] 19 | layerC = tab.layers[textCursor+1] 20 | layerD = tab.layers[textCursor+2] 21 | # Check if all 4 are the same master layers 22 | if layerA.isMasterLayer and layerA.layerId == layerB.layerId == layerC.layerId == layerD.layerId: 23 | return layerA, layerB, layerC, layerD 24 | except: 25 | return False 26 | 27 | 28 | def getIntersection(layer, y, direction = 'left'): 29 | intersections = layer.calculateIntersectionsStartPoint_endPoint_decompose_(NSPoint(-10000, y), NSPoint(10000, y), True)[1:-1] 30 | if not intersections: 31 | return None 32 | if direction == 'left': 33 | return min(intersections, key=lambda point: point.x) 34 | else: 35 | return max(intersections, key=lambda point: point.x) 36 | 37 | 38 | def getTotalDistance(layer1, layer2, y): 39 | try: 40 | # Get distance from the right most intersection to the right side in layer 1 41 | intersection1 = getIntersection(layer1, y, direction = 'right') 42 | distance1 = layer1.width - intersection1.x 43 | 44 | # Get distance from the left most intersection to the left side in layer 2 45 | intersection2 = getIntersection(layer2, y, direction = 'left') 46 | distance2 = intersection2.x 47 | totalDistance = distance1 + distance2 48 | 49 | # Apply kerning, if any 50 | kerningValue = font.kerningForPair( layer1.layerId, layer1.parent.rightKerningKey, layer2.parent.leftKerningKey, direction = LTR ) 51 | if not (kerningValue and kerningValue < 10000): 52 | kerningValue = 0 53 | 54 | return totalDistance, kerningValue 55 | except: 56 | return None, None 57 | 58 | def getKerningKeys(layer1, layer2): 59 | # check exceptions 60 | if Glyphs.versionNumber < 3: 61 | exception1 = layer1.rightKerningExeptionForLayer_(layer2) 62 | exception2 = layer2.leftKerningExeptionForLayer_(layer1) 63 | else: 64 | exception1 = layer1.nextKerningExeptionForLayer_direction_(layer2, LTR) 65 | exception2 = layer2.previousKerningExeptionForLayer_direction_(layer1, LTR) 66 | 67 | # get kerning keys, either groups or exceptions 68 | rightKerningKey = layer1.parent.name if exception1 else layer1.parent.rightKerningKey 69 | leftKerningKey = layer2.parent.name if exception2 else layer2.parent.leftKerningKey 70 | return rightKerningKey, leftKerningKey 71 | 72 | def applyKerning(masterId, rightKerningKey, leftKerningKey, value): 73 | if value: 74 | font.setKerningForPair(masterId, rightKerningKey, leftKerningKey, value) 75 | else: 76 | font.removeKerningForPair(masterId, rightKerningKey, leftKerningKey) 77 | 78 | def stealKerning(): 79 | # A tab must be open 80 | if not(font and font.currentTab): 81 | return 82 | 83 | # Get cursor’s Y 84 | cursorPosition = Glyphs.font.currentTab.graphicView().getActiveLocation_(Glyphs.currentEvent()) # NSPoint 85 | y = cursorPosition.y 86 | 87 | # Get layer pairs 88 | try: 89 | layerA, layerB, layerC, layerD = getLayers(font.currentTab) 90 | except: 91 | return 92 | 93 | # Get distances between pairs, including kerning 94 | totalDistance1, kerningValue1 = getTotalDistance(layerA, layerB, y) 95 | totalDistance2, kerningValue2 = getTotalDistance(layerC, layerD, y) 96 | 97 | # Get difference 98 | if totalDistance1 and totalDistance1: 99 | kerningValue = int(totalDistance2 - totalDistance1 + kerningValue2) 100 | else: 101 | print('Could not steal kerning from the next pair: failed to measure the distance at the cursor’s vertical position %s.' % int(y)) 102 | return 103 | 104 | # Apply kerning 105 | rightKerningKey, leftKerningKey = getKerningKeys(layerA, layerB) 106 | applyKerning(layerA.associatedMasterId, rightKerningKey, leftKerningKey, kerningValue) 107 | 108 | # Report 109 | print('Stole kerning %s for %s %s measuring it from %s %s at y=%s' % (kerningValue, rightKerningKey.replace('@MMK_L_', '@'), leftKerningKey.replace('@MMK_R_', '@'), layerC.parent.name, layerD.parent.name, int(y))) 110 | print('Master: %s' % layerA.name) 111 | 112 | stealKerning() -------------------------------------------------------------------------------- /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 177 | -------------------------------------------------------------------------------- /Overlap nodes.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Overlap Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Select two tip nodes to move others to it – used for Right Grotesk special layers with overlaping nodes. 6 | """ 7 | 8 | from Foundation import NSPoint 9 | from math import tan, radians 10 | from GlyphsApp import Glyphs 11 | 12 | font = Glyphs.font 13 | layer = font.selectedLayers[0] 14 | selection = layer.selection 15 | italicAngle = layer.master.italicAngle 16 | 17 | 18 | # from @mekkablue snippets 19 | def italicize(thisPoint, italicAngle=0.0, pivotalY=0.0): # don't change x to y for horizontal / vertical DIRECTION 20 | x = thisPoint.x 21 | yOffset = thisPoint.y - pivotalY # calculate vertical offset 22 | italicAngle = radians(italicAngle) # convert to radians 23 | tangens = tan(italicAngle) # math.tan needs radians 24 | horizontalDeviance = tangens * yOffset # vertical distance from pivotal point 25 | x += horizontalDeviance # x of point that is yOffset from pivotal point 26 | return NSPoint(int(x), thisPoint.y) 27 | 28 | 29 | # move on curves 30 | for node in selection: 31 | if node.nextNode.type != 'offcurve' and node.nextNode.nextNode.type == 'offcurve': 32 | # oncurve 33 | node.nextNode.x = node.x 34 | node.nextNode.y = node.y 35 | node.nextNode.smooth = False 36 | # offcurve 37 | node.nextNode.nextNode.x = italicize(NSPoint(node.x, node.nextNode.nextNode.y), italicAngle, node.y)[0] 38 | 39 | elif node.prevNode.type != 'offcurve' and node.prevNode.prevNode.type == 'offcurve': 40 | # oncurve 41 | node.prevNode.x = node.x 42 | node.prevNode.y = node.y 43 | node.prevNode.smooth = False 44 | # offcurve 45 | node.prevNode.prevNode.x = italicize(NSPoint(node.x, node.prevNode.prevNode.y), italicAngle, node.y)[0] 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | These are scripts for [Glyphs](https://glyphsapp.com/). Some may not work in Glyphs 3, WIP. 4 | Some scripts use Vanilla. 5 | 6 | ## Installation: 7 | Put the scripts into Scripts folder: Library – Application Support - Glyphs - Scripts 8 | (Cmd + Shift + Y) or through Plugin Manager in Glyphs 3. 9 | 10 | ## Align, Reflect and Rotate 11 | Align and reflect nodes and handles. Useful with keyboard shortcuts — I personally use Ctrl + Cmd + Arrow keys. Set it in System Preferences – Keyboard – Shortcuts. 12 | 13 | ### Align scripts features: 14 | * align selected nodes; 15 | * align paths (select the whole path) and components; 16 | * align node to the next / previous node (select one node only); 17 | * align to the next closest measurment line (vertical metrics, half x-height, half cap-height); 18 | * takes into account the italic angle (which is super duper cool!); 19 | * takes into account smooth connection (“green” nodes) 20 | * NEW: with Caps Lock on, you can now align the whole selection to glyph metrics / measurment lines (vertical metrics, half x-height, half cap-height) 21 | 22 | Center Selected Glyphs sets equal left and right sidebearings within the same width for all layers of selected glyphs. 23 | 24 | ## Export to All Formats 25 | Batch-exports the currently active font to otf, ttf, web (woff, woff2) and variable formats. 26 | Options: 27 | * Separate folders for each format 28 | * Separate folders for each familyName 29 | * Export only the current or all open fonts 30 | Added an option to post-process (compress) exporteed otf/ttf to web formats, which is faster than exporting them from Glyphs. Needs FontTools. 31 | To stop exporting, delete or move the folder. 32 | 33 | Beta: exporting multiple “Static Settings”, which allow exporting different versions of the same instances, bulk-applying different custom parameters/names/properties: 34 | 1. Add an instance(s) with "Static Setting" in its name. Deactivate it. 35 | 2. Add custom parameters and/or change properties (such as the family name) of the "Static Setting" instance. You can find-replace in properties like so: Localized Family Name = "Sans->Serif" will replace all found "Sans" with "Serif" in that property of the instances (useful if you already have multiple familly names in the original instances). 36 | 3. On export, the custom parameters and properties of the "Static Setting" instance will be applied to all other instances. 37 | 4. If you want to also export the original instances as-is, add "+" to any of the "Static Setting", meaning it will be exported in addition to the original instances. 38 | 5. To turn a Static Setting off, just remove (or otherwise break) "Static Setting" from the instance name. 39 | 40 | ## Preferred Names 41 | Sets (or cleans) *preferredFamily* and *preferredSubfamily* instance custom parameters. Based on *Font Family Name* and *Instance / Style Name*. Useful for office apps compatibility. 42 | 43 | ## glyphOrder - Paste 44 | A simple way to reorder glyphs with glyphOrder custom parameter. 45 | 1. Copy space separeted glyph names. 46 | 2. Select glyph, in front of which you want to paste them. 47 | 3. Run the script. 48 | If glyphOrder custom parameter is missing, it will be created. 49 | 50 | ## Dark Mode 51 | Toggles dark mode on and off. 52 | 53 | ## Floating Macro Panel 54 | Toggles Macro panel floating (always on top) on and off. 55 | 56 | ## Toggle Axis 1 / Toggle Italic 57 | Toggles between masters across axis number 1 (or 2, 3, 4) or Italic axis (no matter what number it is). Takes into account selected layers in the current tab. Smart enough to toggle special brace layers such as {50, 100, 0} > {50, 100, 15}. 58 | 59 | ## Demo Instance Generator 60 | Generates instances with limited character set (customizable) from active instances. Adds “Demo” suffix, removes features and OT classes depending on the character set. Needs Vanilla. 61 | 62 | ## Text Filter 63 | Removes all characters from a text, except selected ones. Useful for testing WIP fonts with limited character set. Needs Vanilla. 64 | 65 | ## G2 Harmonize 66 | Harmonizes any selected on-curve points. Algorithm found at @simoncozens. Now the same as Green Harmony plugin. 67 | 68 | ## Dangerous Offcurves 69 | Checks if there are any off-curve points (handles) dangerously close to their curve segment (that may cause problems with conversion to True Type bezier for variable fonts). Opens problematic layers in a new tab. Default threshold value is 0.05 units. 70 | 71 | ## Point Counter 72 | Shows how many points are there in each layer of the current glyph. Useful for fixing interpolation. 73 | 74 | ## Kern to max 75 | For the current pair in the edit view, sets the kerning to maximum. Maximum is the half width of the narrower layer in the pair. Useful for kerning (but not overkerning) stuff like .T. or 'A' 76 | 77 | ## Delete Kerning Pair From All Masters 78 | Removes kerning for selected pair(s) from all masters. If the glyph has a kerning group, it will remove kerning for that group. Exceptions (open locks) are ignored because it’s unclear how to treat them, especially if not all masters have that exception. 79 | 80 | ## Reorder Shapes 81 | A better algorithm for correcting order of shapes (paths, components). It orders paths by length, y, and x. It orders components by glyph name, y, x. 82 | Works somewhat more reliably than Glyphs' own shape ordering tool. 83 | 84 | ## Generate Random Alternates Feature 85 | Creates an OpenType feature that randomizes alternatives. 86 | 1. Glyphs from the selected Categories are randomly added to the selected number of Classes (called rand...). 87 | 2. Then glyphs and its alternatives are randomly placed in 'sub... by ...' sequences of the chosen length. 88 | 3. That can be repeated a few times, depending on how many lookups and lines per lookup you choose. 89 | Input format: glyph and its alternatives space separated. Next glyph with its alternatives go to the next line. 90 | The script is sketchy. The randomness depends on the numbers of classes and lines; I’d say start low and increase the values until it feel right. 91 | 92 | ## Fit Zoom 93 | Fits text in current tab into full screen (if Text/Hand tool is selected) or fits current layer (if other tools are being selected). Works weirdly in Glyphs 3. Send help! :) 94 | 95 | ## Overlap Nodes 96 | Very specific tool that helps to solve kinks on terminals between narrow and normal widths. Converts this: 97 | 98 | ![image](https://user-images.githubusercontent.com/60325634/136535807-2c6927ad-ac17-4ab0-9ab2-64e8ee0b0668.png) 99 | 100 | 101 | into this: 102 | 103 | ![image](https://user-images.githubusercontent.com/60325634/136535872-cb9955f3-7462-4798-9fcf-afa402a0ff8a.png) 104 | 105 | 106 | ## Steal kerning from next pair at cursor's Y 107 | Set’s the kerning for the current pair based on the distance between the next 2 glyphs, as measured at the cursor’s vertical position. 108 | Example: to kern AV the same as VA, type AVVA, place the cursor between AV, and point with the mouse at which vertical position you want the same distance between AV and VA. 109 | 110 | 111 | ## Set Vertical Metrics 112 | A manager for the vertical metrics based on Google’s [strategy](https://github.com/googlefonts/gf-docs/blob/main/VerticalMetrics/README.md). 113 | 114 | ### License 115 | Copyright 2020 Alex Slobzheninov. 116 | 117 | Some algorithm input by Simon Cozens (@simoncozens). 118 | Floating window code help by Florian Pircher. 119 | Distribute Nodes Horizontally/Vertically are a slightly modified version of Distribute Nodes by Rainer Erich Scheichelbauer (@mekkablue) 120 | 121 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use the software provided here except in compliance with the License. You may obtain a copy of the License at 122 | 123 | http://www.apache.org/licenses/LICENSE-2.0 124 | 125 | See the License file included in this repository for further details. 126 | -------------------------------------------------------------------------------- /Reflect/Reflect Horizontally.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Reflect Horizontally 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Flip selected nodes and components horizontally. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSComponent 9 | font = Glyphs.font 10 | 11 | layer = font.selectedLayers[0] 12 | mid = layer.selectionBounds[0].x + layer.selectionBounds[1].width / 2 13 | 14 | for element in layer.selection: 15 | element.x = mid - element.x + mid 16 | if isinstance(element, GSComponent): 17 | element.scale = (-element.scale[0], element.scale[1]) 18 | 19 | # update metrics 20 | layer.updateMetrics() 21 | -------------------------------------------------------------------------------- /Reflect/Reflect Vertically.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Reflect Vertically 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Flip selected nodes and components vertically. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSComponent 9 | font = Glyphs.font 10 | 11 | layer = font.selectedLayers[0] 12 | mid = layer.selectionBounds[0].y + layer.selectionBounds[1].height / 2 13 | 14 | for element in layer.selection: 15 | element.y = mid - element.y + mid 16 | if isinstance(element, GSComponent): 17 | element.scale = (element.scale[0], -element.scale[1]) 18 | 19 | # update metrics 20 | layer.updateMetrics() 21 | -------------------------------------------------------------------------------- /Rotate/Rotate 1 CCW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 1 CCW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(1) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate 1 CW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 1 CW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(-1) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate 10 CCW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 10 CCW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(10) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate 10 CW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 10 CW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(-10) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate 45 CCW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 45 CCW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(45) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate 45 CW.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rotate 45 CW 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from Rotate import rotateLayer 9 | 10 | rotateLayer(-45) 11 | -------------------------------------------------------------------------------- /Rotate/Rotate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division, print_function, unicode_literals 3 | 4 | __doc__ = """ 5 | Rotates selected nodes and components. 6 | """ 7 | 8 | from math import sin, cos, radians 9 | 10 | from GlyphsApp import Glyphs, GSComponent 11 | 12 | 13 | 14 | def rotate(origin, point, angle): 15 | 16 | # Rotate a point counterclockwise by a given angle around a given origin. 17 | # The angle should be given in radians. 18 | 19 | ox, oy = origin 20 | px, py = point 21 | 22 | qx = ox + cos(angle) * (px - ox) - sin(angle) * (py - oy) 23 | qy = oy + sin(angle) * (px - ox) + cos(angle) * (py - oy) 24 | return qx, qy 25 | 26 | 27 | def rotateLayer(angleDeg): 28 | font = Glyphs.font 29 | layer = font.selectedLayers[0] 30 | bounds = layer.selectionBounds 31 | 32 | midX = bounds[0].x + bounds[1].width / 2 33 | midY = bounds[0].y + bounds[1].height / 2 34 | origin = (midX, midY) # center of selection 35 | angle = radians(angleDeg) # negative clockwise 36 | selection = layer.selection 37 | 38 | for element in selection: 39 | newX, newY = rotate(origin, (element.x, element.y), angle) 40 | 41 | if isinstance(element, GSComponent): 42 | 43 | # shift matrix 44 | shiftMatrix = [1, 0, 0, 1, -midX, -midY] 45 | element.applyTransform(shiftMatrix) 46 | # rotate 47 | rotationMatrix = [cos(-angle), -sin(-angle), sin(-angle), cos(-angle), 0, 0] 48 | element.applyTransform(rotationMatrix) 49 | # shift back 50 | shiftMatrix = [1, 0, 0, 1, midX, midY] 51 | element.applyTransform(shiftMatrix) 52 | 53 | else: 54 | element.x = newX 55 | element.y = newY 56 | 57 | # update metrics 58 | layer.updateMetrics() 59 | -------------------------------------------------------------------------------- /Testing/Highest & Lowest Glyphs.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Highest & Lowest Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Finds tallest and lowest glyphs / Y coordinates. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | maxY = None 11 | minY = None 12 | highest = None 13 | lowest = None 14 | 15 | font = Glyphs.font 16 | for glyph in font.glyphs: 17 | for layer in glyph.layers: 18 | if not maxY or layer.bounds.origin.y + layer.bounds.size.height > maxY: 19 | maxY = layer.bounds.origin.y + layer.bounds.size.height 20 | highest = layer 21 | if not minY or layer.bounds.origin.y < minY: 22 | minY = layer.bounds.origin.y 23 | lowest = layer 24 | 25 | print('highest: %s' % maxY) 26 | print('lowest: %s' % minY) 27 | font.newTab([highest, lowest]) 28 | -------------------------------------------------------------------------------- /Testing/Text Filter.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Text Filter 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Filters all characters from a text, except for selected ones. Useful for testing WIP fonts. 5 | """ 6 | 7 | import vanilla 8 | import re 9 | from Foundation import NSUserDefaults 10 | from GlyphsApp import Glyphs 11 | 12 | thisFont = Glyphs.font 13 | openNewTab = False 14 | 15 | m = 15 # margin 16 | tm = 35 # top/vertical margin 17 | bm = 50 # bottom margin 18 | glyphBox = 100 #bottom text box 19 | 20 | 21 | class TextFilter(object): 22 | 23 | def __init__(self): 24 | # Window 'self.w': 25 | windowWidth = 350 26 | windowHeight = 360 27 | windowWidthResize = 1200 # user can resize width by this value 28 | windowHeightResize = 500 # user can resize height by this value 29 | self.w = vanilla.FloatingWindow( 30 | (windowWidth, windowHeight), # default window size 31 | "Text Simplifier", # window title 32 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 33 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 34 | autosaveName="save.TextFilter.mainwindow" # stores last window position and size 35 | ) 36 | 37 | # UI elements: 38 | 39 | self.w.text_inpt = vanilla.TextBox((m, m - 3, m, tm), "Input:", sizeStyle='small') 40 | self.w.text_outpt = vanilla.TextBox((m, m, m, tm), "Output:", sizeStyle='small') 41 | self.w.text_glyphs = vanilla.TextBox((m, m, m, tm), "Character set:", sizeStyle='small') 42 | 43 | self.w.inpt = vanilla.TextEditor((m, tm, m, tm), callback=self.SavePreferences, checksSpelling=False) 44 | self.w.outpt = vanilla.TextEditor((m, tm, m, tm), callback=self.SavePreferences, checksSpelling=False, readOnly='True') 45 | self.w.glyphs = vanilla.TextEditor((m, tm, m, tm), callback=self.SavePreferences, checksSpelling=False) 46 | 47 | self.windowResize(None) 48 | self.w.bind("resize", self.windowResize) 49 | 50 | # Run Button: 51 | self.w.runButton = vanilla.Button((-80 - m, -20 - m, -m, m), "Simplify", sizeStyle='regular', callback=self.TextFilter) 52 | self.w.setDefaultButton(self.w.runButton) 53 | 54 | # Reset Button: 55 | self.w.resetButton = vanilla.Button(((-80 - m) * 2, -20 - m, (-80 - m) - m, m), "Reset", sizeStyle='regular', callback=self.ResetParameters) 56 | 57 | # Open in a new Tab checkbox 58 | self.w.newTab = vanilla.CheckBox((m, -20 - m, (-80 - m) - m, m), "Open in a New Tab", sizeStyle='regular', value=False) 59 | 60 | # Load Settings: 61 | if not self.LoadPreferences(): 62 | print("Note: 'Text Simplifier' could not load preferences. Will resort to defaults") 63 | 64 | # Open window and focus on it: 65 | self.w.open() 66 | self.w.makeKey() 67 | 68 | def windowResize(self, sender): 69 | windowWidth = self.w.getPosSize()[2] 70 | adaptiveWidth = windowWidth / 2 - m * 3 / 2 71 | 72 | self.w.text_inpt.setPosSize((m, m - 3, adaptiveWidth, tm)) 73 | self.w.text_outpt.setPosSize((adaptiveWidth + m * 2, m - 3, adaptiveWidth, tm)) 74 | self.w.text_glyphs.setPosSize((m, -glyphBox - bm - tm + m - 3, -bm, -glyphBox)) 75 | 76 | self.w.inpt.setPosSize((m, tm, adaptiveWidth, -glyphBox - bm - tm)) 77 | self.w.outpt.setPosSize((adaptiveWidth + m * 2, tm, adaptiveWidth, -glyphBox - bm - tm)) 78 | self.w.glyphs.setPosSize((m, -glyphBox - bm, -m, glyphBox)) 79 | 80 | 81 | def SavePreferences(self, sender): 82 | try: 83 | Glyphs.defaults["save.TextFilter.inpt"] = self.w.inpt.get() 84 | Glyphs.defaults["save.TextFilter.outpt"] = self.w.outpt.get() 85 | Glyphs.defaults["save.TextFilter.glyphs"] = self.w.glyphs.get() 86 | 87 | except: 88 | return False 89 | 90 | return True 91 | 92 | 93 | def LoadPreferences(self): 94 | try: 95 | NSUserDefaults.standardUserDefaults().registerDefaults_({ 96 | "save.TextFilter.inpt": "", 97 | "save.TextFilter.outpt": "", 98 | "save.TextFilter.glyphs": "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z . , space", 99 | }) 100 | self.w.inpt.set(Glyphs.defaults["save.TextFilter.inpt"]) 101 | self.w.outpt.set(Glyphs.defaults["save.TextFilter.outpt"]) 102 | self.w.glyphs.set(Glyphs.defaults["save.TextFilter.glyphs"]) 103 | 104 | except: 105 | return False 106 | return True 107 | 108 | def ResetParameters(self, sender): 109 | del Glyphs.defaults["save.TextFilter.outpt"] 110 | del Glyphs.defaults["save.TextFilter.glyphs"] 111 | self.w.outpt.set(Glyphs.defaults["save.TextFilter.outpt"]) 112 | self.w.glyphs.set(Glyphs.defaults["save.TextFilter.glyphs"]) 113 | 114 | 115 | def TextFilter(self, sender): 116 | glyphs = self.w.glyphs.get() 117 | inpt = self.w.inpt.get() 118 | outpt = re.sub(r"[^%s]" % glyphs, "", inpt) 119 | 120 | self.w.outpt.set(outpt) 121 | if self.w.newTab.get() == 1: 122 | thisFont.newTab(outpt) 123 | 124 | 125 | TextFilter() 126 | --------------------------------------------------------------------------------