├── .flake8 ├── pyproject.toml ├── App ├── Update Text Preview.py ├── Navigate │ ├── Activate next glyph.py │ └── Activate previous glyph.py ├── Print Window.py ├── Toggle Script Windows.py ├── Close All Tabs of All Open Fonts.py ├── Toggle Macro Window Separator.py ├── Toggle Horizontal-Vertical.py ├── Toggle RTL-LTR.py ├── Line Height Decrease.py ├── Line Height Increase.py ├── Copy Download URL for Current App Version.py ├── Update git Repositories in Scripts Folder.py ├── Set Export Paths to Adobe Fonts Folder.py └── Set Tool Shortcuts.py ├── .vscode └── settings.json ├── Interpolation ├── Other │ ├── Show masters of next glyph.py │ ├── Show masters of previous glyph.py │ ├── New Tab with Masters of Selected Glyphs.py │ ├── Show next instance.py │ ├── Show previous instance.py │ ├── Lines by Master.py │ └── masterNavigation.py ├── New Tab with Special Layers.py ├── Re-interpolate Selected Layers.py ├── Remove All Non-Master Layers.py ├── Reset Axis Mappings.py ├── axisMethods.py └── Report Instance Interpolations.py ├── .style.yapf ├── Features ├── Update Features without Reordering.py ├── Stylistic Sets │ ├── Report ssXX Names.py │ ├── Create ssXX from layer.py │ └── Synchronize ssXX glyphs.py ├── Activate Default Features.py └── Floating Features.py ├── .gitignore ├── Hinting ├── New Tab with Layers with TTDeltas.py ├── Keep Only First Master Hints.py ├── Set TT Stem Hints to Auto.py ├── Set TT Stem Hints to No Stem.py ├── Set blueFuzz to zero for master instances.py ├── BlueFuzzer.py └── Add Hints for Selected Nodes.py ├── Guides ├── Remove Local Guides in Selected Glyphs.py ├── Remove Global Guides in Current Master.py ├── Select All Global Guides.py ├── Select All Local Guides.py └── Guides through All Selected Nodes.py ├── CONTRIBUTORS.txt ├── Spacing ├── New Tab with all Figure Combinations.py ├── Freeze Placeholders.py ├── Remove Metrics Keys.py ├── Center Glyphs.py ├── Reset Alternate Glyph Widths.py ├── New Tab with Fraction Figure Combinations.py ├── Remove Layer-Specific Metrics Keys.py └── Change Metrics by Percentage.py ├── AUTHORS.txt ├── Kerning ├── New Tab with Selected Glyph Combos.py ├── New Tab with Glyphs of Same Kerning Groups.py ├── New Tab with Right Groups.py └── New Tab with All Group Members.py ├── Build Glyphs ├── Build APL Greek.py ├── Build Q from O and _tail.Q.py ├── Build Ldot and ldot.py └── Build small letter SM, TEL.py ├── Font Info ├── Turn Dimensions into Stems.py ├── Set Variable Style Names.py ├── Set WWS Names (Name IDs 21 and 22).py └── Set Style Linking.py ├── Images ├── Reset Image Transformation.py ├── Delete Images.py ├── Add Same Image to Selected Glyphs.py ├── Delete All Images in Font.py ├── Set New Path for Images.py ├── Adjust Image Alpha.py └── Toggle Image Lock.py ├── Compare Frontmost Fonts ├── Compare Metrics of Two Frontmost Fonts.py ├── Compare Glyphsets of Two Frontmost Fonts.py ├── Compare Sidebearings of Two Frontmost Fonts.py ├── Compare Composites of Two Frontmost Fonts.py ├── Compare Kerning Groups of Two Frontmost Fonts.py ├── Report Missing Glyphs in All Open Fonts.py └── compare.py ├── Test └── Report Highest and Lowest Glyphs.py ├── Paths ├── Remove all Open Paths.py ├── Snap selected points to nearest metric in all masters.py ├── Remove Short Segments.py ├── Enlarge Single-Unit Segments.py ├── Distribute Nodes.py ├── Grid Switcher.py └── Interpolate two paths.py ├── Color Fonts ├── New Tabs with Palette Colors.py ├── Cycle CPAL Colors for Selected Glyphs.py ├── Delete Non-Color Layers in Selected Glyphs.py ├── Reverse CPAL Colors for Selected Glyphs.py ├── Merge All Other Masters in Current Master.py ├── Add All Color Layers to Selected Glyphs.py ├── Convert Master Colors to CPAL Palette.py └── Randomly Distribute Shapes on Color Layers.py ├── Anchors ├── Shine Through Anchors.py ├── Propagate Components and Mark Anchoring.py ├── Prefix all exit:entry anchors with a hashtag.py ├── Insert #exit and #entry anchors at sidebearings.py ├── Add missing smart anchors.py ├── Fix Arabic Anchor Order in Ligatures.py ├── Insert exit and entry Anchors to Selected Positional Glyphs.py ├── Replicate Anchors in Suffixed Glyphs.py ├── Insert #exit and #entry on baseline at selected points.py └── Find and Replace in Anchor Names.py ├── Pixelfonts ├── Delete duplicate components.py ├── Reset rotated and mirrored components.py ├── Align Anchors to Grid.py └── Flashify Pixels.py ├── Glyph Names, Notes and Unicode ├── Reset Unicodes.py ├── Color Composites in Shade of Base Glyph.py ├── Reorder Unicodes of Selected Glyphs.py ├── Convert to Uppercase.py └── Convert to Lowercase.py ├── Post Production ├── setBit3.py ├── winfix.py ├── Add Empty DSIG to Most Recent Variable Font Export.py ├── fixpsnames.py ├── otvarLib.py └── fixgdef.py ├── Components ├── Decompose Corner and Cap Components.py ├── Auto-align Composites with Incremental Metrics Keys.py ├── Make Glyph Smart.py ├── Make Component Smart.py └── Propagate Corner Components to Other Masters.py └── DEVREADME.md /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W191, E501, E722, W503, E741, F841, E265, E225 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | [tool.flake8] 4 | max-line-length = 120 -------------------------------------------------------------------------------- /App/Update Text Preview.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Update Text Preview 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = """ 5 | Force-updates the font shown in Window > Text Preview. 6 | """ 7 | 8 | from GlyphsApp import PreviewTextWindow 9 | 10 | PreviewTextWindow.defaultInstance().reloadFont() 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "editor.formatOnSaveMode": "file", 4 | "editor.formatOnSave": false, 5 | "editor.formatOnType": false, 6 | "flake8.args": [ 7 | "--ignore=W191,E501,E722,W503", 8 | "--verbose" 9 | ], 10 | "python.analysis.extraPaths": [".."] 11 | } -------------------------------------------------------------------------------- /App/Navigate/Activate next glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Activate next glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Will activate the next glyph in Edit view. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font = Glyphs.font 11 | if font: 12 | tab = font.currentTab 13 | if tab: 14 | tab.textCursor = (tab.textCursor + 1) % len(tab.text) 15 | -------------------------------------------------------------------------------- /Interpolation/Other/Show masters of next glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Show Masters of Next Glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Shows all masters for the next glyph. 6 | """ 7 | 8 | import masterNavigation as nav 9 | newGlyphName = nav.glyphNameForIndexOffset(+1) 10 | if newGlyphName: 11 | nav.showAllMastersOfGlyphInCurrentTab(newGlyphName) 12 | -------------------------------------------------------------------------------- /App/Navigate/Activate previous glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Activate previous glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Will activate the previous glyph in Edit view. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font = Glyphs.font 11 | if font: 12 | tab = font.currentTab 13 | if tab: 14 | tab.textCursor = (tab.textCursor - 1) % len(tab.text) 15 | -------------------------------------------------------------------------------- /Interpolation/Other/Show masters of previous glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Show Masters of Previous Glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Shows all masters for the previous glyph. 6 | """ 7 | 8 | import masterNavigation as nav 9 | newGlyphName = nav.glyphNameForIndexOffset(-1) 10 | if newGlyphName: 11 | nav.showAllMastersOfGlyphInCurrentTab(newGlyphName) 12 | -------------------------------------------------------------------------------- /App/Print Window.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Print Window 2 | """Print the frontmost window.""" 3 | 4 | from __future__ import print_function 5 | from GlyphsApp import Glyphs, Message 6 | 7 | doc = Glyphs.currentDocument 8 | if doc: 9 | print(doc.windowController().window().contentView().print_(None)) 10 | else: 11 | Message( 12 | title="Print Window Error", 13 | message="Cannot print window: No document open.", 14 | OKButton=None, 15 | ) 16 | -------------------------------------------------------------------------------- /App/Toggle Script Windows.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Toggle Script Windows 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggles visibility of all windows and panels created by Python scripts. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | scriptWindow = Glyphs.delegate().macroPanelController().window() 11 | 12 | if scriptWindow: 13 | scriptWindow.setIsVisible_(not scriptWindow.isVisible()) 14 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | BASED_ON_STYLE = pep8 3 | USE_TABS = true 4 | CONTINUATION_INDENT_WIDTH = 4 5 | CONTINUATION_ALIGN_STYLE = VALIGN-RIGHT 6 | INDENT_BLANK_LINES = false 7 | INDENT_CLOSING_BRACKETS = true 8 | COLUMN_LIMIT = 180 9 | BLANK_LINES_BETWEEN_TOP_LEVEL_IMPORTS_AND_VARIABLES = 0 10 | BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION = 1 11 | SPACES_BEFORE_COMMENT = 1 12 | ALLOW_MULTILINE_DICTIONARY_KEYS = False 13 | EACH_DICT_ENTRY_ON_SEPARATE_LINE = True 14 | FORCE_MULTILINE_DICT = True 15 | SPLIT_ALL_COMMA_SEPARATED_VALUES = False 16 | 17 | -------------------------------------------------------------------------------- /Features/Update Features without Reordering.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Update Features without Reordering 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Refreshes all existing OT features without changing their order or adding new features. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Glyphs.clearLog() 11 | thisFont = Glyphs.font 12 | 13 | for thisFeature in thisFont.features: 14 | thisFeature.update() 15 | print("Feature %s updated." % thisFeature.name) 16 | 17 | print("Done.") 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.vdiff 3 | 4 | *.pyc 5 | 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | *.bak 33 | /Test/samsa-config.js 34 | -------------------------------------------------------------------------------- /Hinting/New Tab with Layers with TTDeltas.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Layers with TTDeltas 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens new tab with layers with deltas. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, TTDELTA 9 | 10 | font = Glyphs.font 11 | layersWithDelta = [] 12 | for thisGlyph in font.glyphs: 13 | for thisLayer in thisGlyph.layers: 14 | for thisHint in thisLayer.hints: 15 | if thisHint.type == TTDELTA: 16 | if thisLayer not in layersWithDelta: 17 | layersWithDelta.append(thisLayer) 18 | font.newTab(layersWithDelta) 19 | -------------------------------------------------------------------------------- /Interpolation/Other/New Tab with Masters of Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Masters of Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new Edit tab containing all masters of selected glyphs. 6 | """ 7 | 8 | import masterNavigation as nav 9 | from GlyphsApp import Glyphs 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | if thisFont and thisFont.selectedLayers: 13 | glyphNames = [layer.parent.name for layer in thisFont.selectedLayers if layer.parent and layer.parent.name] 14 | nav.showAllMastersOfGlyphs(glyphNames) 15 | -------------------------------------------------------------------------------- /Guides/Remove Local Guides in Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Local Guides in Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Delete all local (blue) guides in selected glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | print("Deleting guides in:") 11 | 12 | for thisLayer in Glyphs.font.selectedLayers: 13 | thisGlyph = thisLayer.parent 14 | # thisGlyph.beginUndo() # undo grouping causes crashes 15 | thisLayer.guideLines = None 16 | # thisGlyph.endUndo() # undo grouping causes crashes 17 | print(" %s" % thisGlyph.name) 18 | -------------------------------------------------------------------------------- /Guides/Remove Global Guides in Current Master.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Global Guides in Current Master 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes all global guidelines in the current master. 6 | """ 7 | from GlyphsApp import Glyphs 8 | 9 | thisFont = Glyphs.font # frontmost font 10 | thisFontMaster = thisFont.selectedFontMaster # active master 11 | numberOfGuidelines = len(thisFontMaster.guideLines) 12 | thisFontMaster.guideLines = [] 13 | 14 | print("Deleted %i global guides in Font %s, Master %s." % (numberOfGuidelines, thisFont.familyName, thisFontMaster.name)) 15 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to this repository. 3 | # The AUTHORS file lists the copyright holders; this file 4 | # lists people. For example, company employees are listed here 5 | # but not in AUTHORS, because their company holds their 6 | # copyright. 7 | # 8 | # When adding J Random Contributor's name to this file, 9 | # either J's name or J's organization's name should be 10 | # added to the AUTHORS file. 11 | # 12 | # Names should be added to this file like so: 13 | # Name 14 | # 15 | # Please keep the list sorted. 16 | # (first name; alphabetical order) 17 | 18 | Dave Crossland 19 | -------------------------------------------------------------------------------- /App/Close All Tabs of All Open Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Close All Tabs of All Open Fonts 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Closes all Edit tabs of all fonts currently open in the app. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | closedTabCount = 0 11 | for thisFont in Glyphs.fonts: 12 | while thisFont.tabs: 13 | thisFont.tabs[0].close() 14 | closedTabCount += 1 15 | 16 | # Floating notification: 17 | Glyphs.showNotification( 18 | "No more tabs open", 19 | "%i tab%s closed in %i font%s." % ( 20 | closedTabCount, 21 | "" if closedTabCount == 1 else "s", 22 | len(Glyphs.fonts), 23 | "" if len(Glyphs.fonts) == 1 else "s", 24 | ), 25 | ) 26 | -------------------------------------------------------------------------------- /App/Toggle Macro Window Separator.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Toggle Macro Window Separator 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggles the separator position in the Macro Window between 80% and 20%. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | if Glyphs.versionNumber < 4: 12 | from Foundation import NSHeight 13 | splitview = Glyphs.delegate().macroPanelController().consoleSplitView() 14 | frame = splitview.frame() 15 | height = NSHeight(frame) 16 | currentPos = splitview.positionOfDividerAtIndex_(0) / height 17 | if currentPos > 0.5: 18 | newPos = 0.2 19 | else: 20 | newPos = 0.8 21 | splitview.setPosition_ofDividerAtIndex_(height * newPos, 0) 22 | -------------------------------------------------------------------------------- /App/Toggle Horizontal-Vertical.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Toggle Horizontal-Vertical 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggle frontmost tab between LTR (horizontal) and vertical writing direction. Useful for setting a keyboard shortcuts. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, LTR, LTRTTB 9 | 10 | if Glyphs.font: 11 | thisTab = Glyphs.font.currentTab 12 | if thisTab: 13 | if thisTab.direction == LTR: 14 | newDirection = LTRTTB 15 | else: 16 | newDirection = LTR 17 | thisTab.direction = newDirection 18 | else: 19 | print("ERROR: No Edit tab open. Cannot switch writing direction.") 20 | else: 21 | print("ERROR: No font open. Cannot switch writing direction.") 22 | -------------------------------------------------------------------------------- /Spacing/New Tab with all Figure Combinations.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with all Figure Combinations 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new edit tab and outputs all possible figure combos: 00010203..., 10111213... etc. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | figures = "0123456789" 11 | text = "" 12 | for z1 in figures: 13 | text += z1 14 | for z2 in figures: 15 | text += z2 + z1 16 | text += "\n" 17 | 18 | try: 19 | # opens new Edit tab with figure combos: 20 | thisFont = Glyphs.font 21 | thisFont.newTab(text) 22 | 23 | except: 24 | # in case last line fails, the text is in the macro window: 25 | Glyphs.clearLog() 26 | Glyphs.showMacroWindow() 27 | print(text) 28 | -------------------------------------------------------------------------------- /Guides/Select All Global Guides.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Select All Global Guides 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Selects all global guides. 6 | """ 7 | from GlyphsApp import Glyphs 8 | 9 | thisFont = Glyphs.font # frontmost font 10 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 11 | 12 | 13 | def selectGuidesOnLayer(thisLayer): 14 | thisMaster = thisLayer.master 15 | thisLayer.clearSelection() 16 | for thisGuide in thisMaster.guides: 17 | thisLayer.selection.append(thisGuide) 18 | return len(thisLayer.selection) 19 | 20 | 21 | for thisLayer in selectedLayers: 22 | thisGlyph = thisLayer.parent 23 | numberOfGuides = selectGuidesOnLayer(thisLayer) 24 | print("Selected %i guides in %s" % (numberOfGuides, thisGlyph.name)) 25 | -------------------------------------------------------------------------------- /Guides/Select All Local Guides.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Select All Local Guides in Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Selects all guides in the selected glyph(s). 6 | """ 7 | from GlyphsApp import Glyphs 8 | 9 | thisFont = Glyphs.font # frontmost font 10 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 11 | 12 | 13 | def selectGuidesOnLayer(thisLayer): 14 | thisLayer.clearSelection() 15 | for thisGuide in thisLayer.guides: 16 | thisLayer.selection.append(thisGuide) 17 | return len(thisLayer.selection) 18 | 19 | 20 | for thisLayer in selectedLayers: 21 | thisGlyph = thisLayer.parent 22 | numberOfGuides = selectGuidesOnLayer(thisLayer) 23 | print("Selected %i guides in %s" % (numberOfGuides, thisGlyph.name)) 24 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | # This is the list of mekkablue Glyphs-Scripts authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | 7 | Rainer Erich Scheichelbauer (mekkablue) 8 | Wei Huang (weiweihuanghuang) 9 | Georg Seifert (schriftgestalt) 10 | Tal Leming (typesupply) 11 | Christoph Schindler (hop) 12 | Maciej Ratajski (maciejratajski) 13 | Bruno Herfst (GitBruno) 14 | Mark Frömberg (Mark2Mark) 15 | Rafał Buchner (RafalBuchner) 16 | Johannes Neumeier (kontur) 17 | Marcin Dybaś (dyyybek) 18 | Daniel Gamage (danielgamage) 19 | Alexei Vanyashin (alexeiva) 20 | Mathieu Triay (MathieuLoutre) 21 | Manuel von Gebhardi (Manuel87) 22 | Tamir Hassan (tamirhassan) 23 | Google LLC 24 | -------------------------------------------------------------------------------- /Hinting/Keep Only First Master Hints.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Keep First Master Hints Only 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | In selected glyphs, delete all hints in all layers except for the first master. Respects Bracket Layers. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | selectedGlyphs = [layer.parent for layer in selectedLayers] 13 | firstMasterName = Font.masters[0].name 14 | 15 | Glyphs.clearLog() 16 | print("Only keeping first-master hints in:") 17 | 18 | for thisGlyph in selectedGlyphs: 19 | print("- %s" % thisGlyph.name) 20 | layersToBeProcessed = [layer for layer in thisGlyph.layers if not layer.name.startswith(firstMasterName)] 21 | for layer in layersToBeProcessed: 22 | layer.hints = None 23 | 24 | print("Done.") 25 | -------------------------------------------------------------------------------- /App/Toggle RTL-LTR.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Toggle RTL/LTR 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggle frontmost tab between LTR and RTL writing direction. Useful for setting a keyboard shortcuts. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | # quick and dirty fix for name change in 3121 10 | try: 11 | from GlyphsApp import GSLTR as LTR, GSRTL as RTL 12 | except: 13 | from GlyphsApp import LTR, RTL # type: ignore 14 | 15 | if Glyphs.font: 16 | thisTab = Glyphs.font.currentTab 17 | if thisTab: 18 | if thisTab.direction == LTR: 19 | newDirection = RTL 20 | else: # RTL or TTB 21 | newDirection = LTR 22 | thisTab.direction = newDirection 23 | # else: 24 | # print("ERROR: No Edit tab open. Cannot switch writing direction.") 25 | # else: 26 | # print("ERROR: No font open. Cannot switch writing direction.") 27 | -------------------------------------------------------------------------------- /App/Line Height Decrease.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Decrease Line Height 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Decrease the Edit View line height. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | Font = Glyphs.font # frontmost font 11 | parameterName = "EditView Line Height" 12 | 13 | # set default height: 14 | if not Font.customParameters[parameterName]: 15 | Font.customParameters[parameterName] = 1200 16 | 17 | lineheight = Font.customParameters[parameterName] 18 | 19 | if not lineheight < 100.0: 20 | lineheight *= 0.8 21 | lineheight = round(lineheight) 22 | Font.customParameters[parameterName] = lineheight 23 | if Font.currentTab: 24 | Font.currentTab.forceRedraw() 25 | else: 26 | Message(title="Line Height Error", message="The line height is already below 100 units. Cannot decrease any further.", OKButton=None) 27 | -------------------------------------------------------------------------------- /App/Line Height Increase.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Increase Line Height 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Increase the Edit View line height. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | Font = Glyphs.font # frontmost font 11 | parameterName = "EditView Line Height" 12 | 13 | # set default height: 14 | if not Font.customParameters[parameterName]: 15 | Font.customParameters[parameterName] = 1200 16 | 17 | lineheight = Font.customParameters[parameterName] 18 | 19 | if not lineheight > Font.upm * 10: 20 | lineheight *= 1.25 21 | lineheight = round(lineheight) 22 | Font.customParameters[parameterName] = lineheight 23 | if Font.currentTab: 24 | Font.currentTab.forceRedraw() 25 | else: 26 | Message(title="Line Height Error", message="The line height exceeds the UPM more than tenfold already. Stop it now.", OKButton=None) 27 | -------------------------------------------------------------------------------- /App/Copy Download URL for Current App Version.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Copy Download URL for Current App Version 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Puts the download URL of the current Glyphs app version into your clipboard for easy pasting. 6 | """ 7 | 8 | from AppKit import NSPasteboard, NSStringPboardType 9 | from GlyphsApp import Glyphs, Message 10 | from mekkablue import setClipboard 11 | 12 | 13 | appURL = "https://updates.glyphsapp.com/Glyphs%s-%i.zip" % ( 14 | Glyphs.versionString, 15 | Glyphs.buildNumber, 16 | ) 17 | 18 | if not setClipboard(appURL): 19 | print("Warning: could not set clipboard to %s" % ("clipboard text")) 20 | Message(title="Clipboard Error", message="Could not set the clipboard for whatever reason, so here is the URL:\n%s" % appURL, OKButton=None) 21 | else: 22 | # Floating notification: 23 | Glyphs.showNotification( 24 | "Download link copied", 25 | "Ready for pasting: %s" % appURL, 26 | ) 27 | -------------------------------------------------------------------------------- /Interpolation/New Tab with Special Layers.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Special Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new Edit tab containing all special (bracket & brace) layers. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | 11 | Glyphs.clearLog() # clears log of Macro window 12 | thisFont = Glyphs.font # frontmost font 13 | affectedLayers = [] 14 | for thisGlyph in thisFont.glyphs: # loop through all glyphs 15 | for thisLayer in thisGlyph.layers: # loop through all layers 16 | # collect affected layers: 17 | if thisLayer.isSpecialLayer: 18 | affectedLayers.append(thisLayer) 19 | 20 | # open a new tab with the affected layers: 21 | if affectedLayers: 22 | newTab = thisFont.newTab() 23 | newTab.layers = affectedLayers 24 | # otherwise send a message: 25 | else: 26 | Message(title="Nothing Found", message="Could not find any bracket or brace layers in the font.", OKButton=None) 27 | -------------------------------------------------------------------------------- /Kerning/New Tab with Selected Glyph Combos.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Selected Glyph Combinations 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new tab with all possible combinations of currently selected glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | namesOfSelectedGlyphs = ["/%s" % layer.parent.name for layer in selectedLayers if hasattr(layer.parent, 'name')] 14 | editString = "" 15 | 16 | for leftGlyphName in namesOfSelectedGlyphs: 17 | for rightGlyphName in namesOfSelectedGlyphs: 18 | newPair = leftGlyphName + rightGlyphName 19 | if newPair not in editString: 20 | editString += newPair 21 | editString += (leftGlyphName + "\n") 22 | 23 | # in case last line fails, the text is in the macro window: 24 | Glyphs.clearLog() # clears macro window log 25 | print(editString) 26 | 27 | # opens new Edit tab: 28 | Font.newTab(editString) 29 | -------------------------------------------------------------------------------- /Hinting/Set TT Stem Hints to Auto.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set TT Stem Hints to Auto 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Sets all TT stem hints to ‘Auto’ in selected glyphs. 6 | """ 7 | 8 | from Foundation import NSNotFound 9 | from GlyphsApp import Glyphs, TTSTEM 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 13 | 14 | 15 | def process(thisLayer): 16 | returnValue = False 17 | for thisHint in thisLayer.hints: 18 | if thisHint.type == TTSTEM: 19 | thisHint.setStem_(NSNotFound) 20 | returnValue = True 21 | return returnValue 22 | 23 | 24 | for thisLayer in listOfSelectedLayers: 25 | thisGlyph = thisLayer.parent 26 | # thisGlyph.beginUndo() # undo grouping causes crashes 27 | if process(thisLayer): 28 | print("%s: OK." % thisGlyph.name) 29 | else: 30 | print("%s: no TT stems found." % thisGlyph.name) 31 | # thisGlyph.endUndo() # undo grouping causes crashes 32 | -------------------------------------------------------------------------------- /Hinting/Set TT Stem Hints to No Stem.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set TT Stem Hints to No Stem 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Sets all TT stem hints to ‘no stem’ in selected glyphs. In complex paths, it can improve rendering on Windows. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, TTSTEM 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 12 | 13 | 14 | def process(thisLayer): 15 | returnValue = False 16 | for thisHint in thisLayer.hints: 17 | if thisHint.type == TTSTEM: 18 | thisHint.setStem_(-1) 19 | returnValue = True 20 | return returnValue 21 | 22 | 23 | for thisLayer in listOfSelectedLayers: 24 | thisGlyph = thisLayer.parent 25 | # thisGlyph.beginUndo() # undo grouping causes crashes 26 | if process(thisLayer): 27 | print("%s: OK." % thisGlyph.name) 28 | else: 29 | print("%s: no TT stems found." % thisGlyph.name) 30 | # thisGlyph.endUndo() # undo grouping causes crashes 31 | -------------------------------------------------------------------------------- /Interpolation/Other/Show next instance.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Show Next Instance 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Jumps to next instance shown in the preview field or window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font = Glyphs.font 11 | numberOfInstances = len(font.instances) 12 | 13 | # Preview Area at the bottom of Edit view: 14 | previewingTab = font.currentTab 15 | 16 | # Window > Preview Panel: 17 | previewPanel = Glyphs.delegate().pluginForClassName_("GlyphsPreviewPanel") 18 | 19 | try: 20 | currentInstanceNumber = previewingTab.selectedInstance() 21 | if currentInstanceNumber < numberOfInstances - 1: 22 | newInstanceNumber = currentInstanceNumber + 1 23 | else: 24 | newInstanceNumber = -2 25 | 26 | previewingTab.setSelectedInstance_(newInstanceNumber) 27 | if previewPanel: 28 | previewPanel.setSelectedInstance_(newInstanceNumber) 29 | 30 | except Exception as e: 31 | print("Error:", e) 32 | import traceback 33 | print(traceback.format_exc()) 34 | -------------------------------------------------------------------------------- /Build Glyphs/Build APL Greek.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Build APL Greek 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Create APL Greek glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSGlyph, GSComponent 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 12 | aplGlyphNames = ("APLiota", "APLrho", "APLomega", "APLalpha") 13 | Glyphs.clearLog() 14 | 15 | for glyphName in aplGlyphNames: 16 | original = glyphName.replace("APL", "") 17 | if thisFont.glyphs[original]: 18 | thisGlyph = thisFont.glyphs[glyphName] 19 | if not thisGlyph: 20 | thisGlyph = GSGlyph() 21 | thisGlyph.name = glyphName 22 | thisFont.glyphs.append(thisGlyph) 23 | 24 | for thisLayer in thisGlyph.layers: 25 | thisLayer.clear() 26 | comp = GSComponent(original) 27 | thisLayer.components.append(comp) 28 | comp.automaticAlignment = True 29 | else: 30 | print("%s: not found in font." % original) 31 | Glyphs.showMacroWindow() 32 | -------------------------------------------------------------------------------- /Kerning/New Tab with Glyphs of Same Kerning Groups.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Glyphs of Same Kerning Groups 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Opens a new tab containing all members of the left and right kerning groups of the current glyph. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | thisGlyph = thisFont.selectedLayers[0].parent 12 | 13 | if thisGlyph: 14 | leftGroup = thisGlyph.leftKerningGroup 15 | rightGroup = thisGlyph.rightKerningGroup 16 | 17 | leftGroupText = "left:\n" 18 | rightGroupText = "right:\n" 19 | 20 | for g in thisFont.glyphs: 21 | if g.leftKerningGroup == leftGroup: 22 | leftGroupText += "/%s" % g.name 23 | if g.rightKerningGroup == rightGroup: 24 | rightGroupText += "/%s" % g.name 25 | 26 | thisFont.newTab("%s %s\n\n%s %s" % (thisGlyph.name, leftGroupText, thisGlyph.name, rightGroupText)) 27 | else: 28 | Message(title="Script Error", message="No glyph currently selected.", OKButton=None) 29 | -------------------------------------------------------------------------------- /Interpolation/Other/Show previous instance.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Show Previous Instance 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Jumps to previous instance shown in the preview field or window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font = Glyphs.font 11 | numberOfInstances = len(font.instances) 12 | 13 | # Preview Area at the bottom of Edit view: 14 | previewingTab = font.currentTab 15 | 16 | # Window > Preview Panel: 17 | 18 | previewPanel = Glyphs.delegate().pluginForClassName_("GlyphsPreviewPanel") 19 | 20 | try: 21 | currentInstanceNumber = previewingTab.selectedInstance() 22 | if currentInstanceNumber > -2: 23 | newInstanceNumber = currentInstanceNumber - 1 24 | else: 25 | newInstanceNumber = numberOfInstances - 1 26 | 27 | previewingTab.setSelectedInstance_(newInstanceNumber) 28 | if previewPanel: 29 | previewPanel.setSelectedInstance_(newInstanceNumber) 30 | 31 | except Exception as e: 32 | print("Error:", e) 33 | import traceback 34 | print(traceback.format_exc()) 35 | -------------------------------------------------------------------------------- /Font Info/Turn Dimensions into Stems.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Turn Dimensions into Stems 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Turns all H and V dimensions (in the Dimensions palette) into H and V stems. 6 | """ 7 | 8 | from GlyphsApp import GSMetric, GSInfoValue 9 | 10 | font = Glyphs.font 11 | dimensions = font.userData["GSDimensionPlugin.Dimensions"] 12 | 13 | for masterID in dimensions.keys(): 14 | master = font.fontMasterForId_(masterID) 15 | for dimKey in dimensions[masterID].keys(): 16 | if dimKey[-1] not in "HV": 17 | continue 18 | if not font.stems or dimKey not in [s.name for s in font.stems]: 19 | stem = GSMetric() 20 | stem.name = dimKey 21 | stem.type = 0 22 | stem.horizontal = dimKey[-1] == "H" 23 | font.addStem_(stem) 24 | else: 25 | stem = font.stems[dimKey] 26 | value = dimensions[masterID][dimKey] 27 | info = GSInfoValue.alloc().initWithValue_(int(value) or 0) 28 | master.setStemValue_forId_(info, stem.id) 29 | 30 | font.parent.windowController().showFontInfoWindowWithTabSelected_(1) -------------------------------------------------------------------------------- /Interpolation/Other/Lines by Master.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Lines by Master 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Reduplicates your edit text across masters, will add one line per master. Careful, ignores everything after the first newline. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSControlLayer 9 | 10 | thisFont = Glyphs.font 11 | 12 | glyphs3 = Glyphs.versionNumber >= 3 13 | cutoff = [] 14 | names = [] 15 | for i, l in enumerate(thisFont.currentTab.layers): 16 | if isinstance(l, GSControlLayer): 17 | cutoff.append(i) 18 | else: 19 | if not cutoff: 20 | names.append(l.parent.name) 21 | 22 | theseLayers = [] 23 | for m in thisFont.masters: 24 | for gname in names: 25 | layer = thisFont.glyphs[gname].layers[m.id] 26 | # print(layer) 27 | theseLayers.append(layer) 28 | 29 | theseLayers.append(GSControlLayer.newline()) 30 | 31 | if theseLayers: 32 | if glyphs3: 33 | thisFont.currentTab.layers.extend(theseLayers) 34 | else: 35 | for layer in theseLayers: 36 | thisFont.currentTab.layers.append(layer) 37 | -------------------------------------------------------------------------------- /Images/Reset Image Transformation.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reset Image Transformations 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Resets all placed images to 100% scale and 0/0 position. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | 14 | def process(thisLayer): 15 | thisImage = thisLayer.backgroundImage 16 | if thisImage: 17 | thisImage.transform = ((1.0, 0.0, 0.0, 1.0, 0.0, 0.0)) 18 | 19 | 20 | Font.disableUpdateInterface() 21 | try: 22 | for thisLayer in selectedLayers: 23 | thisGlyph = thisLayer.parent 24 | print("Resetting image in", thisGlyph.name) 25 | # thisGlyph.beginUndo() # undo grouping causes crashes 26 | process(thisLayer) 27 | # thisGlyph.endUndo() # undo grouping causes crashes 28 | except Exception as e: 29 | Glyphs.showMacroWindow() 30 | print("\n⚠️ Script Error:\n") 31 | import traceback 32 | print(traceback.format_exc()) 33 | print() 34 | raise e 35 | finally: 36 | Font.enableUpdateInterface() # re-enables UI updates in Font View 37 | -------------------------------------------------------------------------------- /Kerning/New Tab with Right Groups.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Right Groups 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates a new tab with one glyph of each right group. Useful for checking the constency of right kerning groups. 6 | """ 7 | from PyObjCTools.AppHelper import callAfter 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | thisFontMaster = thisFont.selectedFontMaster # active master 12 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 13 | 14 | groupDict = {} 15 | 16 | for thisGlyph in thisFont.glyphs: 17 | glyphName = thisGlyph.name 18 | rGroup = thisGlyph.rightKerningGroup 19 | if rGroup in groupDict.keys(): 20 | groupDict[rGroup].append(glyphName) 21 | else: 22 | groupDict[rGroup] = [glyphName] 23 | 24 | tabString = "" 25 | 26 | for thisRightGroup in groupDict.keys(): 27 | tabString += "/%s/space\n" % "/".join(groupDict[thisRightGroup]) 28 | 29 | # opens new Edit tab: 30 | callAfter(Glyphs.currentDocument.windowController().addTabWithString_, tabString) 31 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Compare Metrics of Two Frontmost Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Compare Metrics 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Compare widths of two frontmost fonts. Tolerates 2 unit 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | font1 = Glyphs.font # frontmost font 11 | font2 = Glyphs.fonts[1] # other font 12 | 13 | # brings macro window to front and clears its log: 14 | Glyphs.clearLog() 15 | Glyphs.showMacroWindow() 16 | print(f"Comparing:\nFont 1: {font1.filepath}\nFont 2: {font2.filepath}\n") 17 | 18 | masterIndexesInBothFonts = range(min(len(font1.masters), len(font2.masters))) 19 | for g1 in [g for g in font1.glyphs if g.export]: 20 | glyphname = g1.name 21 | g2 = font2.glyphs[glyphname] 22 | if g2: 23 | for mi in masterIndexesInBothFonts: 24 | m1 = font1.masters[mi] 25 | m2 = font2.masters[mi] 26 | l1 = g1.layers[m1.id] 27 | l2 = g2.layers[m2.id] 28 | if abs(l1.width - l2.width) > 2.0: 29 | print("/%s : widths: %.1f <> %.1f (%s)" % (glyphname, l1.width, l2.width, m1.name)) 30 | else: 31 | print(" %s not in font 2" % (glyphname)) 32 | -------------------------------------------------------------------------------- /Features/Stylistic Sets/Report ssXX Names.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Report ssXX Names of All Open Fonts 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Opens Macro Window with a list of all stylistic set names. 5 | """ 6 | 7 | from GlyphsApp import Glyphs 8 | 9 | Glyphs.clearLog() # clears log of Macro window 10 | Glyphs.showMacroWindow() 11 | print("Names for ssXX:") 12 | 13 | 14 | def instanceIsActive(instance): 15 | if Glyphs.buildNumber > 3198: 16 | return instance.exports 17 | else: 18 | return instance.active 19 | 20 | 21 | # reversed, so that italics are sorted after uprights: 22 | sortedFonts = reversed(sorted(Glyphs.fonts, key=lambda font: font.filepath.lastPathComponent())) 23 | for font in sortedFonts: 24 | print() 25 | # heuristics for determining if it is an italic: 26 | italic = all(["italic" in i.name.lower() for i in font.instances if instanceIsActive(i)]) 27 | sortedFeatures = sorted(font.features, key=lambda feature: feature.name) 28 | for feature in sortedFeatures: 29 | if feature.name.startswith("ss"): 30 | print(f'{font.familyName} {"Italic" if italic else ""}, {feature.name}: {feature.featureNamesString().splitlines()[1].strip()[6:-2]}'.replace(" ,", ",")) 31 | -------------------------------------------------------------------------------- /Test/Report Highest and Lowest Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Report Highest and Lowest Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Reports highest and lowest glyphs for each master in the Macro Window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font 11 | exportingGlyphs = [g for g in thisFont.glyphs if g.export] 12 | 13 | Glyphs.clearLog() 14 | Glyphs.showMacroWindow() 15 | 16 | fontname = thisFont.familyName 17 | if thisFont.filepath: 18 | fontname = thisFont.filepath.lastPathComponent() 19 | print("Highest and lowest glyphs for %s\n" % fontname) 20 | for thisMaster in thisFont.masters: 21 | masterID = thisMaster.id 22 | glyphsBottomsAndTops = [[g.name, g.layers[masterID].bounds.origin.y, g.layers[masterID].bounds.origin.y + g.layers[masterID].bounds.size.height] for g in exportingGlyphs] 23 | lowest = sorted(glyphsBottomsAndTops, key=lambda x: x[1])[0] 24 | highest = sorted(glyphsBottomsAndTops, key=lambda x: -x[2])[0] 25 | print("Master: %s" % thisMaster.name) 26 | print("⬆️ %s (%.1f)" % (highest[0], highest[2])) 27 | print("⬇️ %s (%.1f)" % (lowest[0], lowest[1])) 28 | print() 29 | -------------------------------------------------------------------------------- /Hinting/Set blueFuzz to zero for master instances.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set blueFuzz to zero for master instances 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds blueFuzz custom parameter with value 0 for instances that are the same as a master. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, INSTANCETYPESINGLE 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | axesValuesOfAllMasters = [m.axes for m in thisFont.masters] 12 | Glyphs.clearLog() # clears log in Macro window 13 | 14 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 15 | try: 16 | for thisInstance in [i for i in thisFont.instances if i.type == INSTANCETYPESINGLE]: 17 | if thisInstance.axes in axesValuesOfAllMasters: 18 | thisInstance.customParameters["blueFuzz"] = 0 19 | print(f"ℹ️ {thisInstance.name}: blueFuzz 0") 20 | except Exception as e: 21 | Glyphs.showMacroWindow() 22 | print("\n⚠️ Error in script: Set blueFuzz to zero for master instances\n") 23 | import traceback 24 | print(traceback.format_exc()) 25 | print() 26 | raise e 27 | finally: 28 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 29 | -------------------------------------------------------------------------------- /Spacing/Freeze Placeholders.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Freeze Placeholders 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Turn placeholders in current tab into current glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | try: 11 | thisFont = Glyphs.font # frontmost font 12 | currentTab = thisFont.currentTab # current edit tab, if any 13 | selectedGlyph = thisFont.selectedLayers[0].parent # active layers of selected glyphs 14 | 15 | if currentTab: 16 | currentTab.text = currentTab.text.replace("/Placeholder", "/%s" % selectedGlyph.name) 17 | else: 18 | Message(title="Cannot Freeze Placeholders", message="You must have an edit tab open, and a glyph selected. Otherwise, the script cannot work.", OKButton="Got it") 19 | except Exception as e: # noqa: F841 20 | # brings macro window to front and clears its log: 21 | Glyphs.clearLog() 22 | import traceback 23 | print(traceback.format_exc()) 24 | Message( 25 | title="Freezing Placeholders Failed", 26 | message="An error occurred during the execution of the script. Is a font open, a glyph selected? Check the Macro Window for a detailed error message.", 27 | OKButton=None 28 | ) 29 | -------------------------------------------------------------------------------- /Interpolation/Re-interpolate Selected Layers.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Re-interpolate Selected Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Batch-reinterpolates all selected layers. Same as the Re-Interpolate command in the Layers palette, but for multiple selections. 6 | """ 7 | 8 | thisFont = Glyphs.font # frontmost font 9 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 10 | Glyphs.clearLog() # clears log in Macro window 11 | print(f"👩🏼‍🔬 Reinterpolating {len(selectedLayers)} layers:\n") 12 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 13 | try: 14 | for thisLayer in selectedLayers: 15 | thisGlyph = thisLayer.parent 16 | print(f"- {thisGlyph.name}, layer: {thisLayer.name}") 17 | thisGlyph.beginUndo() # begin undo grouping 18 | thisLayer.reinterpolate() 19 | thisGlyph.endUndo() # end undo grouping 20 | except Exception as e: 21 | Glyphs.showMacroWindow() 22 | print("\n⚠️ Error in script: Re-interpolate Selected Layers\n") 23 | import traceback 24 | print(traceback.format_exc()) 25 | print() 26 | raise e 27 | finally: 28 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 29 | -------------------------------------------------------------------------------- /Paths/Remove all Open Paths.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove all Open Paths 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes all paths in visible layers of selected glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | 14 | def process(thisLayer): 15 | count = 0 16 | 17 | # thisLayer.parent.beginUndo() # undo grouping causes crashes 18 | for i in range(len(thisLayer.paths))[::-1]: 19 | if not thisLayer.paths[i].closed: 20 | thisPath = thisLayer.paths[i] 21 | if Glyphs.versionNumber >= 3: 22 | index = thisLayer.shapes.index(thisPath) 23 | del thisLayer.shapes[index] 24 | else: 25 | del thisLayer.paths[i] 26 | count += 1 27 | # thisLayer.parent.endUndo() # undo grouping causes crashes 28 | 29 | return count 30 | 31 | 32 | Font.disableUpdateInterface() 33 | try: 34 | for thisLayer in selectedLayers: 35 | print("Removing %i open paths in %s." % (process(thisLayer), thisLayer.parent.name)) 36 | except Exception as e: 37 | Glyphs.showMacroWindow() 38 | print("\n⚠️ Script Error:\n") 39 | import traceback 40 | print(traceback.format_exc()) 41 | print() 42 | raise e 43 | finally: 44 | Font.enableUpdateInterface() # re-enables UI updates in Font View 45 | -------------------------------------------------------------------------------- /Font Info/Set Variable Style Names.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Set Variable Style Names 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Adds Style Names for variable fonts to instances in Font Info > Exports, and makes an informed guess as for their value. Useful if you split your static family in subfamilies (e.g. by optical size or by width), and as a result, you end up with repeating style names (e.g. multiple Mediums). 6 | """ 7 | 8 | thisFont = Glyphs.font # frontmost font 9 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 10 | try: 11 | thisFont = Glyphs.font 12 | for thisInstance in thisFont.instances: 13 | if thisInstance.type == INSTANCETYPEVARIABLE: 14 | continue 15 | part1 = thisInstance.preferredFamily.replace(thisFont.familyName, "").strip() 16 | part2 = thisInstance.name.strip() 17 | if part1 and part2 == "Regular": 18 | thisInstance.variableStyleName = part1 19 | else: 20 | thisInstance.variableStyleName = f"{part1} {part2}".strip() 21 | except Exception as e: 22 | Glyphs.showMacroWindow() 23 | print("\n⚠️ Error in script: Set Variable Style Names\n") 24 | import traceback 25 | print(traceback.format_exc()) 26 | print() 27 | raise e 28 | finally: 29 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 30 | -------------------------------------------------------------------------------- /Images/Delete Images.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Images 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes placed images from selected glyphs on all layers. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | 14 | def process(thisGlyph): 15 | deleteCount = 0 16 | for thisLayer in thisGlyph.layers: 17 | if thisLayer.backgroundImage: 18 | thisLayer.setBackgroundImage_(None) 19 | deleteCount += 1 20 | return deleteCount 21 | 22 | 23 | Font.disableUpdateInterface() 24 | try: 25 | print("Removing images in %s selected glyphs ..." % len(selectedLayers)) 26 | 27 | for thisLayer in selectedLayers: 28 | thisGlyph = thisLayer.parent 29 | # thisGlyph.beginUndo() # undo grouping causes crashes 30 | numberOfDeletedImages = process(thisGlyph) 31 | if numberOfDeletedImages: 32 | plural = 0 33 | if numberOfDeletedImages > 1: 34 | plural = 1 35 | print(" Deleted %i image%s in %s." % (numberOfDeletedImages, "s" * plural, thisGlyph.name)) 36 | # thisGlyph.endUndo() # undo grouping causes crashes 37 | 38 | except Exception as e: 39 | Glyphs.showMacroWindow() 40 | print("\n⚠️ Script Error:\n") 41 | import traceback 42 | print(traceback.format_exc()) 43 | print() 44 | raise e 45 | 46 | finally: 47 | Font.enableUpdateInterface() 48 | -------------------------------------------------------------------------------- /Font Info/Set WWS Names (Name IDs 21 and 22).py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set WWS Names (Name IDs 21 and 22) 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Sets WWS custom parameters (Name IDs 21 and 22) for all instances where necessary: Puts all info except RIBBI into the WWSFamilyName, and only keeps RIBBI for the WWSSubfamilyName. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | wwsStyles = ("Regular", "Bold", "Italic", "Bold Italic") 13 | 14 | for thisInstance in thisFont.instances: 15 | print("Processing Instance:", thisInstance.name) 16 | familyName = thisFont.familyName 17 | if thisInstance.customParameters["familyName"]: 18 | familyName = thisInstance.customParameters["familyName"] 19 | if thisInstance.name not in wwsStyles: 20 | wwsSubFamily = "Regular" 21 | for wwsStyle in ("Bold", "Italic", "Bold Italic"): 22 | if wwsStyle in thisInstance.name: 23 | wwsSubFamily = wwsStyle 24 | 25 | familyNameAddition = thisInstance.name.replace(wwsSubFamily, "").strip().replace(" ", " ") 26 | wwsFamilyName = familyName.strip() + " " + familyNameAddition.strip() 27 | thisInstance.customParameters["WWSFamilyName"] = wwsFamilyName 28 | thisInstance.customParameters["WWSSubfamilyName"] = wwsSubFamily 29 | print(" WWSFamilyName:", wwsFamilyName) 30 | print(" WWSSubfamilyName:", wwsSubFamily) 31 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Compare Glyphsets of Two Frontmost Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Compare Glyphsets 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Compares the glyph set of the two frontmost fonts and outputs a report in the Macro Window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.fonts[0] # frontmost font 11 | otherFont = Glyphs.fonts[1] # second font 12 | 13 | thisFileName = thisFont.filepath.pathComponents()[-1] 14 | otherFileName = otherFont.filepath.pathComponents()[-1] 15 | 16 | thisGlyphSet = [g.name for g in thisFont.glyphs if g.export] 17 | otherGlyphSet = [g.name for g in otherFont.glyphs if g.export] 18 | 19 | for i in range(len(thisGlyphSet))[::-1]: 20 | if thisGlyphSet[i] in otherGlyphSet: 21 | otherGlyphSet.remove(thisGlyphSet.pop(i)) 22 | 23 | for i in range(len(otherGlyphSet))[::-1]: 24 | if otherGlyphSet[i] in thisGlyphSet: 25 | thisGlyphSet.remove(otherGlyphSet.pop(i)) 26 | 27 | # brings macro window to front and clears its log: 28 | Glyphs.clearLog() 29 | Glyphs.showMacroWindow() 30 | print("1. %s\n%s\n" % (thisFont.familyName, thisFont.filepath)) 31 | print("2. %s\n%s\n" % (otherFont.familyName, otherFont.filepath)) 32 | print("Glyphs not in %s:\n" % thisFileName) 33 | print(", ".join(otherGlyphSet)) 34 | print() 35 | print("Glyphs not in %s:\n" % otherFileName) 36 | print(", ".join(thisGlyphSet)) 37 | print() 38 | -------------------------------------------------------------------------------- /Features/Activate Default Features.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Activate Default Features 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | In the current Edit tab, activates all OT features that should be on by default. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | defaultFeatures = """ 12 | abvf 13 | abvm 14 | abvs 15 | akhn 16 | blwf 17 | blwm 18 | blws 19 | calt 20 | ccmp 21 | cfar 22 | cjct 23 | clig 24 | cpsp 25 | curs 26 | dist 27 | fin2 28 | fin3 29 | fina 30 | half 31 | haln 32 | init 33 | isol 34 | kern 35 | liga 36 | ljmo 37 | locl 38 | lfbd 39 | mark 40 | med2 41 | medi 42 | mkmk 43 | nukt 44 | opbd 45 | pref 46 | pres 47 | pstf 48 | psts 49 | rclt 50 | rlig 51 | rkrf 52 | rphf 53 | stch 54 | tjmo 55 | vjmo 56 | ltra 57 | ltrm 58 | rtla 59 | rtlm 60 | valt 61 | vrt2 62 | dtls 63 | flac 64 | ssty 65 | """ 66 | 67 | thisFont = Glyphs.font # frontmost font 68 | defaultFeatures = defaultFeatures.strip().splitlines() 69 | availableDefaultFeatures = [f.name for f in thisFont.features if f.name in defaultFeatures] 70 | 71 | editTab = thisFont.currentTab 72 | for featureName in availableDefaultFeatures: 73 | if featureName not in editTab.selectedFeatures(): 74 | if Glyphs.versionNumber < 3: 75 | editTab.selectedFeatures().append(featureName) 76 | else: 77 | editTab.selectedFeatures().addObject_(featureName) 78 | 79 | editTab.graphicView().reflow() 80 | editTab._updateFeaturePopup() 81 | -------------------------------------------------------------------------------- /Paths/Snap selected points to nearest metric in all masters.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Snap selected points to nearest metric in all masters 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division, print_function, unicode_literals 5 | 6 | __doc__ = """ 7 | Select points and run this script to snap them to the nearest metric in each compatible layer, given they are no more than 2 units away from the metric. Reports in the Macro window. 8 | """ 9 | 10 | from GlyphsApp import Glyphs, GSNode 11 | 12 | threshold = 2 13 | alignedNodesCount = 0 14 | 15 | for selectedLayer in Glyphs.font.selectedLayers: 16 | glyph = selectedLayer.parent 17 | originalCompareString = selectedLayer.compareString() 18 | 19 | for node in selectedLayer.selection: 20 | if not isinstance(node, GSNode): 21 | continue 22 | pathIndex = selectedLayer.indexPathOfNode_(node)[0] 23 | nodeIndex = selectedLayer.indexPathOfNode_(node)[1] 24 | 25 | for layer in glyph.layers: 26 | if layer.compareString() != originalCompareString: 27 | continue 28 | 29 | node = layer.paths[pathIndex].nodes[nodeIndex] 30 | metrics = sorted(set([m.position for m in layer.metrics])) 31 | for metric in metrics: 32 | diff = abs(node.y - metric) 33 | if diff != 0 and diff <= threshold: 34 | node.y = metric 35 | alignedNodesCount += 1 36 | break 37 | 38 | print(f"Aligned {alignedNodesCount} points in {', '.join([selectedLayer.parent.name for l in Glyphs.font.selectedLayers])}.") 39 | -------------------------------------------------------------------------------- /Paths/Remove Short Segments.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Short Segments 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes single-unit segments. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSOFFCURVE 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 12 | 13 | 14 | def process(thisLayer): 15 | for thisPath in thisLayer.paths: 16 | for i in range(len(thisPath.nodes))[::-1]: 17 | thisNode = thisPath.nodes[i] 18 | prevNode = thisNode.prevNode 19 | if prevNode.type != GSOFFCURVE and thisNode.type != GSOFFCURVE: 20 | xDistance = thisNode.x - prevNode.x 21 | yDistance = thisNode.y - prevNode.y 22 | if abs(xDistance) < 1.0 and abs(yDistance) < 1.0: 23 | thisPath.removeNodeCheckKeepShape_(thisNode) 24 | 25 | 26 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 27 | try: 28 | for thisLayer in selectedLayers: 29 | thisGlyph = thisLayer.parent 30 | print("Processing %s" % thisGlyph.name) 31 | # thisGlyph.beginUndo() # undo grouping causes crashes 32 | process(thisLayer) 33 | # thisGlyph.endUndo() # undo grouping causes crashes 34 | except Exception as e: 35 | Glyphs.showMacroWindow() 36 | print("\n⚠️ Script Error:\n") 37 | import traceback 38 | print(traceback.format_exc()) 39 | print() 40 | raise e 41 | finally: 42 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 43 | -------------------------------------------------------------------------------- /Paths/Enlarge Single-Unit Segments.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Enlarge Short Segments 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Doubles single-unit distances. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSOFFCURVE 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 12 | 13 | 14 | def process(thisLayer): 15 | for thisPath in thisLayer.paths: 16 | for thisNode in thisPath.nodes: 17 | prevNode = thisNode.prevNode 18 | if prevNode.type != GSOFFCURVE and thisNode.type != GSOFFCURVE: 19 | xDistance = thisNode.x - prevNode.x 20 | yDistance = thisNode.y - prevNode.y 21 | 22 | if abs(xDistance) <= 1.0 and abs(yDistance) <= 1.0: 23 | thisNode.x = prevNode.x + xDistance * 2 24 | thisNode.y = prevNode.y + yDistance * 2 25 | 26 | 27 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 28 | try: 29 | for thisLayer in selectedLayers: 30 | thisGlyph = thisLayer.parent 31 | print("Processing %s" % thisGlyph.name) 32 | # thisGlyph.beginUndo() # undo grouping causes crashes 33 | process(thisLayer) 34 | # thisGlyph.endUndo() # undo grouping causes crashes 35 | except Exception as e: 36 | Glyphs.showMacroWindow() 37 | print("\n⚠️ Script Error:\n") 38 | import traceback 39 | print(traceback.format_exc()) 40 | print() 41 | raise e 42 | finally: 43 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 44 | -------------------------------------------------------------------------------- /Font Info/Set Style Linking.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set Style Linking 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Tries to set Bold/Italic bits in Font Info > Exports. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | 12 | 13 | def guessStyleLinking(thisInstance): 14 | regular = "Regular" 15 | bold = "Bold" 16 | italic = "Italic" 17 | thisInstance.isBold = False 18 | thisInstance.isItalic = False 19 | thisInstance.linkStyle = "" 20 | 21 | if italic in thisInstance.name: 22 | thisInstance.isItalic = True 23 | linkStyleName = thisInstance.name.replace(italic, "").strip() 24 | if linkStyleName == bold: 25 | thisInstance.isBold = True 26 | thisInstance.linkStyle = regular 27 | else: 28 | thisInstance.linkStyle = linkStyleName 29 | 30 | elif thisInstance.name == bold: 31 | thisInstance.isBold = True 32 | thisInstance.linkStyle = regular 33 | 34 | 35 | for thisInstance in thisFont.instances: 36 | print( 37 | "BEFORE: %s is %s %s of '%s'" % 38 | (thisInstance.name, "Bold" if thisInstance.isBold else "-", "Italic" if thisInstance.isItalic else "-", thisInstance.linkStyle if thisInstance.linkStyle else "-") 39 | ) 40 | guessStyleLinking(thisInstance) 41 | print( 42 | "AFTER: %s is %s %s of '%s'\n" % 43 | (thisInstance.name, "Bold" if thisInstance.isBold else "-", "Italic" if thisInstance.isItalic else "-", thisInstance.linkStyle if thisInstance.linkStyle else "-") 44 | ) 45 | -------------------------------------------------------------------------------- /Images/Add Same Image to Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Same Image to Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Asks you for an image file and inserts it as background image into all selected layers. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSBackgroundImage, GetOpenFile 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | 14 | def process(thisLayer, imageFilePath): 15 | try: 16 | thisImage = GSBackgroundImage.alloc().initWithPath_(imageFilePath) 17 | thisLayer.setBackgroundImage_(thisImage) 18 | except Exception as e: 19 | if "NoneType" in str(e): 20 | return "No image found." 21 | else: 22 | return "Error: %s." % e 23 | return "OK." 24 | 25 | 26 | Font.disableUpdateInterface() 27 | try: 28 | imageFilePath = GetOpenFile(message="Select an image:", allowsMultipleSelection=False, filetypes=["jpeg", "png", "tif", "gif", "pdf"]) 29 | 30 | print("Putting %s into:" % imageFilePath) 31 | 32 | for thisLayer in selectedLayers: 33 | thisGlyph = thisLayer.parent 34 | # thisGlyph.beginUndo() # undo grouping causes crashes 35 | print("-- %s: %s" % (thisGlyph.name, process(thisLayer, imageFilePath))) 36 | # thisGlyph.endUndo() # undo grouping causes crashes 37 | 38 | except Exception as e: 39 | Glyphs.showMacroWindow() 40 | print("\n⚠️ Script Error:\n") 41 | import traceback 42 | print(traceback.format_exc()) 43 | print() 44 | raise e 45 | 46 | finally: 47 | Font.enableUpdateInterface() 48 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Compare Sidebearings of Two Frontmost Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Compare Sidebearings 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Compare sidebearings of two frontmost fonts. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font1 = Glyphs.font # frontmost font 11 | Font2 = Glyphs.fonts[1] # other font 12 | tolerance = 2.0 13 | 14 | # brings macro window to front and clears its log: 15 | Glyphs.clearLog() 16 | Glyphs.showMacroWindow() 17 | 18 | print("Comparing:\nFont 1: %s\nFont 2: %s\n" % (Font1.filepath, Font2.filepath)) 19 | 20 | count = 0 21 | for g1 in [g for g in Font1.glyphs if g.export]: 22 | glyphname = g1.name 23 | g2 = Font2.glyphs[glyphname] 24 | if g2: 25 | for mi, m1 in enumerate(Font1.masters): 26 | m2 = Font2.masters[mi] 27 | l1 = g1.layers[m1.id] 28 | l2 = g2.layers[m2.id] 29 | if not l1.isAligned and not l2.isAligned: 30 | reportGlyph = False 31 | reportString = "/%s : " % glyphname 32 | if abs(l1.LSB - l2.LSB) > tolerance: 33 | reportGlyph = True 34 | reportString += "LSB %i <> %i " % (l1.LSB, l2.LSB) 35 | if abs(l1.RSB - l2.RSB) > tolerance: 36 | reportGlyph = True 37 | reportString += "RSB %i <> %i " % (l1.RSB, l2.RSB) 38 | if reportGlyph: 39 | count += 1 40 | print("%s (%s)" % (reportString, m1.name)) 41 | else: 42 | print(" %s not in Font 2" % (glyphname)) 43 | 44 | print("Found %i discrepancies beyond %i units in all masters." % (count, tolerance)) 45 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Compare Composites of Two Frontmost Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Compare Composites 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Reports diverging component structures of composite glyphs, e.g., iacute built with acutecomb in one font, and acutecomb.narrow in the other. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font1 = Glyphs.font 11 | Font2 = Glyphs.fonts[1] 12 | 13 | filePath1 = "~/" + Font1.filepath.relativePathFromBaseDirPath_("~") 14 | filePath2 = "~/" + Font2.filepath.relativePathFromBaseDirPath_("~") 15 | fileName1 = Font1.filepath.lastPathComponent() 16 | fileName2 = Font2.filepath.lastPathComponent() 17 | 18 | # brings macro window to front and clears its log: 19 | Glyphs.clearLog() 20 | Glyphs.showMacroWindow() 21 | 22 | print("Comparing composites:\nFont 1: %s\nFont 2: %s\n\nFont 1: %s\nFont 2: %s\n" % (fileName1, fileName2, filePath1, filePath2)) 23 | 24 | for g1 in [g for g in Font1.glyphs if g.export]: 25 | glyphname = g1.name 26 | g2 = Font2.glyphs[glyphname] 27 | if g2: 28 | for mi, m1 in enumerate(Font1.masters): 29 | m2 = Font2.masters[mi] 30 | l1 = g1.layers[m1.id] 31 | l2 = g2.layers[m2.id] 32 | 33 | composite1 = "+".join([c.componentName for c in l1.components]) 34 | composite2 = "+".join([c.componentName for c in l2.components]) 35 | 36 | if composite1 != composite2: 37 | print("/%s : %s <> %s" % (glyphname, composite1, composite2)) 38 | else: 39 | print(" %s not in ‘%s’" % (glyphname, fileName2)) 40 | -------------------------------------------------------------------------------- /Interpolation/Remove All Non-Master Layers.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove All Non-Master Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Goes through selected glyphs and deletes all glyph layers which are not a Master, Bracket or Brace layer. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayers = Font.selectedLayers 12 | 13 | 14 | def process(thisGlyph): 15 | count = 0 16 | numberOfLayers = len(thisGlyph.layers) 17 | for i in range(numberOfLayers)[::-1]: 18 | thisLayer = thisGlyph.layers[i] 19 | if not thisLayer.isMasterLayer and not thisLayer.isSpecialLayer: 20 | thisLayerShouldBeRemoved = True 21 | if thisLayerShouldBeRemoved: 22 | count += 1 23 | del thisGlyph.layers[i] 24 | return count 25 | 26 | 27 | Glyphs.clearLog() # clears macro window log 28 | 29 | excludedGlyphNameBeginnings = ("_smart", "_part") 30 | for thisLayer in selectedLayers: 31 | thisGlyph = thisLayer.parent 32 | thisGlyphName = thisGlyph.name 33 | 34 | nameIsAnException = False 35 | for prefix in excludedGlyphNameBeginnings: 36 | if thisGlyphName.startswith(prefix): 37 | nameIsAnException = True 38 | 39 | if not nameIsAnException: 40 | # thisGlyph.beginUndo() # undo grouping causes crashes 41 | count = process(thisGlyph) 42 | if count > 0: 43 | print("%s layers deleted in %s." % (count, thisGlyphName)) 44 | # thisGlyph.endUndo() # undo grouping causes crashes 45 | else: 46 | print("Smart layers kept in %s." % (thisGlyphName)) 47 | -------------------------------------------------------------------------------- /Images/Delete All Images in Font.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove All Images from Font 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes all placed images from the frontmost font. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font 11 | 12 | 13 | def process(thisGlyph): 14 | deleteCount = 0 15 | # thisGlyph.beginUndo() # undo grouping causes crashes 16 | 17 | for thisLayer in thisGlyph.layers: 18 | try: 19 | if thisLayer.backgroundImage: 20 | thisLayer.setBackgroundImage_(None) 21 | deleteCount += 1 22 | except Exception as e: 23 | print(" ⚠️ %s, layer ‘%s’: %s\n" % (thisGlyph.name, thisLayer.name, e)) 24 | 25 | # thisGlyph.endUndo() # undo grouping causes crashes 26 | return deleteCount 27 | 28 | 29 | thisFont.disableUpdateInterface() 30 | try: 31 | print("Removing images in %s glyphs ..." % len(thisFont.glyphs)) 32 | 33 | totalCount = 0 34 | 35 | for thisGlyph in thisFont.glyphs: 36 | numberOfDeletedImages = process(thisGlyph) 37 | plural = min(numberOfDeletedImages, 1) # 0 or 1 38 | print(" Deleted %i image%s in %s." % (numberOfDeletedImages, "s" * plural, thisGlyph.name)) 39 | totalCount += numberOfDeletedImages 40 | 41 | except Exception as e: 42 | Glyphs.showMacroWindow() 43 | print("\n⚠️ Script Error:\n") 44 | import traceback 45 | print(traceback.format_exc()) 46 | print() 47 | raise e 48 | 49 | finally: 50 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 51 | 52 | print("Removed links to %i images in total." % totalCount) 53 | -------------------------------------------------------------------------------- /Spacing/Remove Metrics Keys.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Metrics Keys 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes left and right metrics keys, in all layers of all selected glyphs. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | Glyphs.clearLog() # clears macro window log 12 | thisFont = Glyphs.font # frontmost font 13 | thisFontMaster = thisFont.selectedFontMaster # active master 14 | listOfSelectedLayers = [layer for layer in thisFont.selectedLayers if hasattr(layer.parent, 'name')] 15 | # active layers of selected glyphs 16 | 17 | 18 | def process(thisGlyph): 19 | thisGlyph.setLeftMetricsKey_(None) 20 | thisGlyph.setRightMetricsKey_(None) 21 | thisGlyph.setWidthMetricsKey_(None) 22 | for thisLayer in thisGlyph.layers: 23 | thisLayer.setLeftMetricsKey_(None) 24 | thisLayer.setRightMetricsKey_(None) 25 | thisLayer.setWidthMetricsKey_(None) 26 | 27 | 28 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 29 | try: 30 | for thisLayer in listOfSelectedLayers: 31 | thisGlyph = thisLayer.parent 32 | print("Deleted metrics keys: %s" % thisGlyph.name) 33 | # thisGlyph.beginUndo() # undo grouping causes crashes 34 | process(thisGlyph) 35 | # thisGlyph.endUndo() # undo grouping causes crashes 36 | 37 | except Exception as e: 38 | Glyphs.showMacroWindow() 39 | print("\n⚠️ Script Error:\n") 40 | import traceback 41 | print(traceback.format_exc()) 42 | print() 43 | raise e 44 | 45 | finally: 46 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 47 | -------------------------------------------------------------------------------- /Color Fonts/New Tabs with Palette Colors.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: New Tabs with Palette Colors 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Opens new tabs, one each for all layers pertaining to a color index (CPAL/COLR). 6 | """ 7 | 8 | def countColors(font): 9 | colors = [] 10 | palettes = font.customParameters["Color Palettes"] 11 | if not palettes: 12 | return None 13 | palette = palettes[0] 14 | return len(palette) 15 | 16 | def newTabWithColorIndex(font, requestedIndex=0, includeInactive=False, colorKey="colorPalette"): 17 | currentMasterID = font.selectedFontMaster.id 18 | layers = [] 19 | for glyph in font.glyphs: 20 | if not glyph.export and not includeInactive: 21 | continue 22 | for layer in glyph.layers: 23 | if layer.associatedMasterId != currentMasterID: 24 | continue 25 | if not layer.attributes: 26 | continue 27 | if not colorKey in layer.attributes.keys(): 28 | continue 29 | paletteIndex = layer.attributes[colorKey] 30 | if paletteIndex == requestedIndex: 31 | layers.append(layer) 32 | tab = font.newTab() 33 | tab.layers = layers 34 | return len(layers) 35 | 36 | Glyphs.clearLog() # clears log of Macro window 37 | affectedLayers = 0 38 | thisFont = Glyphs.font # frontmost font 39 | for colorIndex in range(countColors(thisFont)): 40 | affectedLayers += newTabWithColorIndex(thisFont, requestedIndex=colorIndex) 41 | 42 | 43 | # open a new tab with the affected layers: 44 | if affectedLayers == 0: 45 | Message( 46 | title = "Nothing Found", 47 | message = f"Could not find any glyphs with CPAL layers in font ‘{thisFont.familyName}’.", 48 | OKButton = None 49 | ) 50 | 51 | -------------------------------------------------------------------------------- /Anchors/Shine Through Anchors.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Shine Through Anchors 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | In all layers of selected glyphs, inserts (‘traversing’) anchors from components. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def process(thisGlyph): 12 | insertedAnchors = [] 13 | layerCount = 0 14 | for thisLayer in thisGlyph.layers: 15 | if thisLayer.isMasterLayer or thisLayer.isSpecialLayer: 16 | layerCount += 1 17 | for thisAnchor in thisLayer.anchorsTraversingComponents(): 18 | thisLayer.anchors.append(thisAnchor.copy()) 19 | insertedAnchors.append(thisAnchor.name) 20 | 21 | insertedAnchors = sorted(list(set(insertedAnchors))) 22 | print("\t⚓️ Added %i anchors on %i layers: %s" % ( 23 | len(insertedAnchors), 24 | layerCount, 25 | ", ".join(insertedAnchors), 26 | )) 27 | 28 | 29 | thisFont = Glyphs.font # frontmost font 30 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 31 | Glyphs.clearLog() # clears log in Macro window 32 | 33 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 34 | try: 35 | for thisLayer in selectedLayers: 36 | thisGlyph = thisLayer.parent 37 | print("🔠 %s" % thisGlyph.name) 38 | thisGlyph.beginUndo() # begin undo grouping 39 | process(thisGlyph) 40 | thisGlyph.endUndo() # end undo grouping 41 | except Exception as e: 42 | Glyphs.showMacroWindow() 43 | print("\n⚠️ Error in script: Shine Through Anchors\n") 44 | import traceback 45 | print(traceback.format_exc()) 46 | print() 47 | raise e 48 | finally: 49 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 50 | -------------------------------------------------------------------------------- /Pixelfonts/Delete duplicate components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Delete Duplicate Components 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Delete components of the same base glyph and in the same position. Useful for accidental double 5 | """ 6 | 7 | def process(thisLayer): 8 | for i in range(len(thisLayer.shapes)-1, 0, -1): 9 | shape = thisLayer.shapes[i] 10 | if not isinstance(shape, GSComponent): 11 | continue 12 | for j in range(i-1, -1, -1): 13 | otherShape = thisLayer.shapes[j] 14 | if not isinstance(otherShape, GSComponent): 15 | continue 16 | areSameComponent = shape.componentName == otherShape.componentName 17 | areInSameSpot = shape.position == otherShape.position 18 | if areInSameSpot and areSameComponent: 19 | del thisLayer.shapes[i] 20 | break 21 | 22 | 23 | Glyphs.clearLog() # clears log in Macro window 24 | thisFont = Glyphs.font # frontmost font 25 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 26 | 27 | print(f"Deleting duplicate components in {thisFont.familyName}...\n") 28 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 29 | try: 30 | for thisLayer in set(selectedLayers): 31 | thisGlyph = thisLayer.parent 32 | print(f"🔠 {thisGlyph.name}, layer ‘{thisLayer.name}’") 33 | thisGlyph.beginUndo() # begin undo grouping 34 | process(thisLayer) 35 | thisGlyph.endUndo() # end undo grouping 36 | print("\n✅ Done.") 37 | except Exception as e: 38 | Glyphs.showMacroWindow() 39 | print("\n⚠️ Error in script: Delete Duplicate Components\n") 40 | import traceback 41 | print(traceback.format_exc()) 42 | print() 43 | raise e 44 | finally: 45 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 46 | -------------------------------------------------------------------------------- /Glyph Names, Notes and Unicode/Reset Unicodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reset Unicode Codepoints Based on GlyphData 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | For selected glyphs, it works like Glyph > Update Glyph Info, but will not change the name, rather reset the Unicode. Will process the built-in GlyphData and GlyphData-XXX.xml in ~/Library/Application Support/Glyphs 3/Info/. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 12 | 13 | Glyphs.clearLog() # clears log in Macro window 14 | print("Reset Unicode Codepoints Based on GlyphData:") 15 | print(f"Processing {len(selectedLayers)} selected glyphs in {thisFont.familyName}...") 16 | 17 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 18 | try: 19 | for thisLayer in selectedLayers: 20 | thisGlyph = thisLayer.parent 21 | thisGlyph.beginUndo() # begin undo grouping 22 | 23 | newCode = Glyphs.glyphInfoForName(thisGlyph.name).unicode 24 | if newCode: 25 | thisGlyph.unicode = Glyphs.glyphInfoForName(thisGlyph.name).unicode 26 | print(f"🔢 {thisGlyph.unicode} {thisGlyph.name}") 27 | else: 28 | print(f"❌ no Unicode codepoint available for {thisGlyph.name}") 29 | 30 | thisGlyph.endUndo() # end undo grouping 31 | print("✅ Done.") 32 | except Exception as e: 33 | Glyphs.showMacroWindow() 34 | print("\n⚠️ Error in script: Reset Unicode Codepoints Based on GlyphData\n") 35 | import traceback 36 | print(traceback.format_exc()) 37 | print() 38 | raise e 39 | finally: 40 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 41 | -------------------------------------------------------------------------------- /Spacing/Center Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Center Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Center all selected glyphs inside their respective widths. 6 | """ 7 | 8 | from AppKit import NSAffineTransform 9 | from GlyphsApp import Glyphs, GSNode 10 | 11 | 12 | def shiftMatrix(xShift): 13 | transform = NSAffineTransform.transform() 14 | transform.translateXBy_yBy_(xShift, 0) 15 | return transform.transformStruct() 16 | 17 | 18 | Font = Glyphs.font 19 | Font.disableUpdateInterface() 20 | try: 21 | selectedLayers = Font.selectedLayers 22 | if len(selectedLayers) == 1 and selectedLayers[0].selection: 23 | currentLayer = selectedLayers[0] 24 | selectionOrigin = currentLayer.selectionBounds.origin.x 25 | selectionWidth = currentLayer.selectionBounds.size.width 26 | shift = shiftMatrix((currentLayer.width - selectionWidth) * 0.5 - selectionOrigin) 27 | for item in currentLayer.selection: 28 | try: 29 | if isinstance(item, GSNode): 30 | item.x += shift.tX 31 | else: 32 | item.applyTransform(shift) 33 | except Exception as e: 34 | print(e) 35 | else: 36 | for thisLayer in selectedLayers: 37 | thisMaster = thisLayer.master 38 | shift = shiftMatrix((thisLayer.LSB - thisLayer.RSB) * -0.5) 39 | thisLayer.applyTransform(shift) 40 | 41 | except Exception as e: 42 | Glyphs.showMacroWindow() 43 | print("\n⚠️ 'Center Glyphs' Script Error:\n") 44 | import traceback 45 | print(traceback.format_exc()) 46 | print() 47 | raise e 48 | 49 | finally: 50 | Font.enableUpdateInterface() # re-enables UI updates in Font View 51 | 52 | print("✅ Centered: %s" % (", ".join([layer.parent.name for layer in selectedLayers]))) 53 | -------------------------------------------------------------------------------- /Post Production/setBit3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from fontTools.ttLib import TTFont 4 | 5 | def modify_head_flags(font_path, output_path, bit3_value=1, bit13_value=1): 6 | with TTFont(font_path) as font: 7 | head = font['head'] 8 | 9 | if bit3_value == 1: 10 | head.flags |= 1 << 3 # Set bit 3 (value 8) 11 | else: 12 | head.flags &= ~(1 << 3) # Clear bit 3 13 | 14 | if bit13_value == 1: 15 | head.flags |= 1 << 13 # Set bit 13 (value 8192) 16 | else: 17 | head.flags &= ~(1 << 13) # Clear bit 13 18 | 19 | font.save(output_path) 20 | 21 | if __name__ == "__main__": 22 | parser = argparse.ArgumentParser( 23 | description='Modify head.flags bits 3 and 13 in OpenType fonts', 24 | ) 25 | parser.add_argument( 26 | 'fonts', 27 | nargs='+', 28 | help='input font files', 29 | ) 30 | parser.add_argument( 31 | '-b', 32 | '--bit3', 33 | type=int, 34 | choices=[0,1], 35 | default=1, 36 | help='set bit 3 ‘integer scaling’ value (0 or 1, default=1)', 37 | ) 38 | parser.add_argument( 39 | '-c', 40 | '--bit13', 41 | type=int, 42 | choices=[0,1], 43 | default=1, 44 | help='set bit 13 ‘ClearType’ value (0 or 1, default=1)', 45 | ) 46 | parser.add_argument( 47 | '-o', 48 | '--output', 49 | help='output file (if not specified, will overwrite input file)', 50 | ) 51 | 52 | args = parser.parse_args() 53 | 54 | for font_path in args.fonts: 55 | output = args.output or font_path 56 | modify_head_flags(font_path, output, args.bit3, args.bit13) 57 | if font_path != output: 58 | print(f"✅ Updated bit3={args.bit3}, bit13={args.bit13} in {font_path} -> {output}") 59 | else: 60 | print(f"✅ Updated bit3={args.bit3}, bit13={args.bit13} in {font_path}") 61 | print() -------------------------------------------------------------------------------- /Images/Set New Path for Images.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set New Path for Images 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Resets the path for placed images in selected glyphs. Useful if you have moved your images. 6 | """ 7 | 8 | import os 9 | from GlyphsApp import Glyphs, GetFolder 10 | 11 | 12 | def process(thisLayer): 13 | try: 14 | thisImage = thisLayer.backgroundImage 15 | thisImageFileName = os.path.basename(thisImage.path) 16 | thisImageNewFullPath = "%s/%s" % (newFolder, thisImageFileName) 17 | thisImage.setImagePath_(thisImageNewFullPath) 18 | except Exception as e: 19 | if "NoneType" in str(e): 20 | return "No image found." 21 | else: 22 | return "Error: %s." % e 23 | 24 | return "new path %s" % thisImageNewFullPath 25 | 26 | 27 | Font = Glyphs.font 28 | FontMaster = Font.selectedFontMaster 29 | selectedLayers = Font.selectedLayers 30 | newFolder = GetFolder(message="Choose location of placed images:", allowsMultipleSelection=False) 31 | 32 | Glyphs.clearLog() 33 | Glyphs.showMacroWindow() 34 | Font.disableUpdateInterface() 35 | try: 36 | if newFolder: 37 | print("New image path for selected glyphs:\n%s" % newFolder) 38 | for thisLayer in selectedLayers: 39 | thisGlyph = thisLayer.parent 40 | # thisGlyph.beginUndo() # undo grouping causes crashes 41 | print("-- %s: %s" % (thisGlyph.name, process(thisLayer))) 42 | # thisGlyph.endUndo() # undo grouping causes crashes 43 | 44 | except Exception as e: 45 | Glyphs.showMacroWindow() 46 | print("\n⚠️ Script Error:\n") 47 | import traceback 48 | print(traceback.format_exc()) 49 | print() 50 | raise e 51 | 52 | finally: 53 | Font.enableUpdateInterface() # re-enables UI updates in Font View 54 | -------------------------------------------------------------------------------- /App/Update git Repositories in Scripts Folder.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Update git Repositories in Scripts Folder 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Executes a 'git pull' command on all subfolders in the Glyphs Scripts folder. Will not work for the Glyphs 3 Repositories folder, which you need to take care of with the Plugin Manager. 6 | """ 7 | 8 | import os 9 | from GlyphsApp import Glyphs, GSGlyphsInfo, Message 10 | 11 | if Glyphs.versionNumber >= 3: 12 | # GLYPHS 3 13 | Message( 14 | title="Friendly Reminder", 15 | message="In Glyphs 3 and later, we strongly recommend to manage your scripts with the Plugin Manager (Window → Plugin Manager). Do not manage the script repositories yourself. This script only works on the Scripts folder, not the Repositories folder.", 16 | OKButton=None, 17 | ) 18 | 19 | Glyphs.clearLog() 20 | if Glyphs.versionNumber >= 3: 21 | # GLYPHS 3 22 | scriptsFolderPath = os.path.join(GSGlyphsInfo.applicationSupportPath(), "Scripts") 23 | else: 24 | # GLYPHS 2 25 | scriptsFolderPath = os.path.expanduser("~/Library/Application Support/Glyphs/Scripts") 26 | 27 | expandedScriptsFolderPath = os.path.expanduser(scriptsFolderPath) 28 | print("Executing 'git pull' for all subfolders in:\n%s" % expandedScriptsFolderPath) 29 | 30 | os.chdir(expandedScriptsFolderPath) 31 | exitStatus = os.system(r'find . -mindepth 1 -maxdepth 1 -type d -print -exec git -C {} pull -f \; -exec echo \;') 32 | os.system('open "%s"' % scriptsFolderPath) 33 | 34 | if exitStatus != 0: 35 | print("ERROR: Exit Status %i" % exitStatus) 36 | Glyphs.showMacroWindow() 37 | else: 38 | print("Done.") 39 | Glyphs.showNotification( 40 | "Completed git pull", 41 | scriptsFolderPath, 42 | ) 43 | -------------------------------------------------------------------------------- /Interpolation/Reset Axis Mappings.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reset Axis Mappings 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Inserts (or resets) a default Axis Mappings parameter for all style values currently present in the font. Ignores style values outside the designspace bounds defined by the masters. 6 | """ 7 | 8 | from Foundation import NSMutableDictionary 9 | from axisMethods import extremeMasterValuesNative, nativeMasterExtremes, styleValueForAxisTag 10 | from GlyphsApp import Glyphs, Message 11 | 12 | if Glyphs.versionNumber < 3.0: 13 | Message(title="Glyphs Version Error", message="This script requires Glyphs 3.0 or later.", OKButton=None) 14 | # return 15 | 16 | mappings = NSMutableDictionary.alloc().init() 17 | font = Glyphs.font 18 | for axis in font.axes: 19 | axisTag = axis.axisTag 20 | minAxisPos, maxAxisPos = extremeMasterValuesNative(font, axisTag=axisTag) 21 | 22 | # add axis extremes: 23 | axisMapping = NSMutableDictionary.alloc().init() 24 | for masterExtreme in nativeMasterExtremes: 25 | axisMapping.addObject_forKey_(masterExtreme, masterExtreme) 26 | 27 | # add style positions 28 | for style in font.instances: 29 | styleValue = styleValueForAxisTag(style, axisTag) 30 | if minAxisPos < styleValue < maxAxisPos: 31 | axisMapping.addObject_forKey_(styleValue, styleValue) 32 | 33 | # add this axis mapping to mappings: 34 | mappings.addObject_forKey_(axisMapping, axisTag) 35 | 36 | parameterName = "Axis Mappings" 37 | 38 | # backup old parameter: 39 | existingParameter = font.customParameterForKey_(parameterName) 40 | if existingParameter: 41 | existingParameter.name = "OLD %s" % parameterName 42 | 43 | # write new parameter: 44 | font.customParameters[parameterName] = mappings 45 | -------------------------------------------------------------------------------- /Spacing/Reset Alternate Glyph Widths.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reset Alternate Glyph Widths 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Sets the width of selected .ss01 (or any other extension) widths in the font to the width of their base glyphs. E.g. A.ss01 will have the same width as A. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | FontMaster = Font.selectedFontMaster 12 | selectedLayers = Font.selectedLayers 13 | 14 | 15 | def resetWidth(thisLayer, thisName): 16 | if thisLayer is None: 17 | print("> couldn't get layer of <%s> " % thisName) 18 | return 19 | baseGlyphName = thisName[:thisName.find(".")] 20 | baseGlyph = Font.glyphs[baseGlyphName] 21 | if baseGlyph is None: 22 | print("> couldn't find a base glyph for <%s> " % thisName) 23 | return 24 | baseLayer = baseGlyph.layers[FontMaster.id] 25 | baseWidth = baseLayer.width 26 | thisLayer.width = baseWidth 27 | return baseWidth 28 | 29 | 30 | Font.disableUpdateInterface() 31 | try: 32 | for thisLayer in selectedLayers: 33 | thisGlyph = thisLayer.parent 34 | thisGlyphName = thisGlyph.name 35 | if "." in thisGlyphName: 36 | thisLayer.parent.beginUndo() # undo grouping causes crashes 37 | try: 38 | print("Resetting width of %s to %.0f." % (thisGlyphName, resetWidth(thisLayer, thisGlyphName))) 39 | except: 40 | print("> ERROR, couldn't reset <%s>" % (thisGlyphName)) 41 | thisLayer.parent.endUndo() # undo grouping causes crashes 42 | 43 | except Exception as e: 44 | Glyphs.showMacroWindow() 45 | print("\n⚠️ Script Error:\n") 46 | import traceback 47 | print(traceback.format_exc()) 48 | print() 49 | raise e 50 | 51 | finally: 52 | Font.enableUpdateInterface() # re-enables UI updates in Font View 53 | -------------------------------------------------------------------------------- /Anchors/Propagate Components and Mark Anchoring.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Propagate Components and Mark Anchoring 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Takes the current master’s component and mark anchoring setup and replicates it in all other (compatible) masters. Useful for complex Arabic ligature marks. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def process(thisLayer): 12 | componentStructure = thisLayer.componentNames() 13 | glyph = thisLayer.parent 14 | for layer in glyph.layers: 15 | if layer is thisLayer: 16 | continue 17 | layer.setComponentNames_(componentStructure) 18 | if layer.compareString() != thisLayer.compareString(): 19 | continue 20 | for i, accentComponent in enumerate(layer.components): 21 | if i < 1: 22 | continue 23 | accentComponent.setAnchor_(thisLayer.components[i].anchor) 24 | 25 | 26 | thisFont = Glyphs.font # frontmost font 27 | thisFontMaster = thisFont.selectedFontMaster # active master 28 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 29 | Glyphs.clearLog() # clears log in Macro window 30 | 31 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 32 | try: 33 | for thisLayer in selectedLayers: 34 | thisGlyph = thisLayer.parent 35 | print(f"Processing {thisGlyph.name}") 36 | thisGlyph.beginUndo() # begin undo grouping 37 | process(thisLayer) 38 | thisGlyph.endUndo() # end undo grouping 39 | except Exception as e: 40 | Glyphs.showMacroWindow() 41 | print("\n⚠️ Error in script: Propagate Components and Mark Anchoring\n") 42 | import traceback 43 | print(traceback.format_exc()) 44 | print() 45 | raise e 46 | finally: 47 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 48 | -------------------------------------------------------------------------------- /Interpolation/axisMethods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def masterValueForAxisTag(master, axisTag="wght"): 4 | font = master.font 5 | axisID = [a for a in font.axes if a.axisTag == axisTag][0].axisId 6 | value = master.axisValueValueForId_(axisID) 7 | return value 8 | 9 | 10 | def styleValueForAxisTag(style, axisTag="wght"): 11 | font = style.font 12 | axisID = [a for a in font.axes if a.axisTag == axisTag][0].axisId 13 | value = style.axisValueValueForId_(axisID) 14 | return value 15 | 16 | 17 | def extremeMasterValuesNative(font, axisTag="wght"): 18 | low, high = None, None 19 | for master in font.masters: 20 | masterValue = masterValueForAxisTag(master, axisTag) 21 | if low is None or masterValue < low: 22 | low = masterValue 23 | if high is None or masterValue > high: 24 | high = masterValue 25 | return low, high 26 | 27 | 28 | def extremeStyleValuesNative(font, axisTag="wght"): 29 | low, high = None, None 30 | for style in font.instances: 31 | styleValue = styleValueForAxisTag(style, axisTag) 32 | if low is None or styleValue < low: 33 | low = styleValue 34 | if high is None or styleValue > high: 35 | high = styleValue 36 | return low, high 37 | 38 | 39 | def extremeStyleValuesWeightClass(font, axisTag="wght"): 40 | low, high = None, None 41 | for style in font.instances: 42 | styleValue = style.weightClassValue() 43 | if low is None or styleValue < low: 44 | low = styleValue 45 | if high is None or styleValue > high: 46 | high = styleValue 47 | return low, high 48 | 49 | 50 | def coefficient(number, low, high): 51 | span = high - low 52 | coefficient = (number - low) / span 53 | return coefficient 54 | 55 | 56 | def valueForCoefficient(coefficient, low, high): 57 | span = high - low 58 | number = low + coefficient * span 59 | return number 60 | -------------------------------------------------------------------------------- /Paths/Distribute Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Distribute Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Distributes the selected nodes horizontally or vertically, depending on the bounding box. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | Font = Glyphs.font 11 | selectedLayer = Font.selectedLayers[0] 12 | 13 | try: 14 | selection = selectedLayer.selection 15 | 16 | selectionXList = [n.x for n in selection] 17 | selectionYList = [n.y for n in selection] 18 | leftMostX, rightMostX = min(selectionXList), max(selectionXList) 19 | lowestY, highestY = min(selectionYList), max(selectionYList) 20 | diffX = abs(leftMostX - rightMostX) 21 | diffY = abs(lowestY - highestY) 22 | 23 | Font.disableUpdateInterface() 24 | try: 25 | if diffX > diffY: 26 | increment = diffX / float(len(selection) - 1) 27 | sortedSelection = sorted(selection, key=lambda n: n.x) 28 | for thisNodeIndex in range(len(selection) - 1): 29 | sortedSelection[thisNodeIndex].x = leftMostX + (thisNodeIndex * increment) 30 | else: 31 | increment = diffY / float(len(selection) - 1) 32 | sortedSelection = sorted(selection, key=lambda n: n.y) 33 | for thisNodeIndex in range(len(selection) - 1): 34 | sortedSelection[thisNodeIndex].y = lowestY + (thisNodeIndex * increment) 35 | except Exception as e: 36 | Glyphs.showMacroWindow() 37 | print("\n⚠️ Script Error:\n") 38 | import traceback 39 | print(traceback.format_exc()) 40 | print() 41 | raise e 42 | finally: 43 | Font.enableUpdateInterface() # re-enables UI updates in Font View 44 | 45 | except Exception as e: 46 | if selection == (): 47 | print("Cannot distribute nodes: nothing selected in frontmost layer.") 48 | else: 49 | print("Error. Cannot distribute nodes:", selection) 50 | print(e) 51 | -------------------------------------------------------------------------------- /Components/Decompose Corner and Cap Components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Decompose Corner and Cap Components 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Recreates the current paths without caps or components. Hold down SHIFT to decompose on all layers. 6 | """ 7 | 8 | from AppKit import NSEvent, NSShiftKeyMask 9 | from GlyphsApp import Glyphs 10 | 11 | keysPressed = NSEvent.modifierFlags() 12 | shiftKeyPressed = keysPressed & NSShiftKeyMask == NSShiftKeyMask 13 | 14 | thisFont = Glyphs.font # frontmost font 15 | 16 | 17 | def decomposeCornerAndCapComponentsOnLayer(thisLayer): 18 | thisLayer.decomposeSmartOutlines() 19 | thisLayer.cleanUpPaths() # duplicate nodes at startpoint 20 | 21 | 22 | def decomposeCornerAndCapComponentsOnAllLayersOfGlyph(thisGlyph): 23 | for thisLayer in thisGlyph.layers: 24 | if thisLayer.isSpecialLayer or thisLayer.isMasterLayer: 25 | decomposeCornerAndCapComponentsOnLayer(thisLayer) 26 | 27 | 28 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 29 | try: 30 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 31 | for thisLayer in selectedLayers: 32 | thisGlyph = thisLayer.parent 33 | print("Processing", thisGlyph.name) 34 | # thisGlyph.beginUndo() # undo grouping causes crashes 35 | if shiftKeyPressed: 36 | decomposeCornerAndCapComponentsOnAllLayersOfGlyph(thisGlyph) 37 | else: 38 | decomposeCornerAndCapComponentsOnLayer(thisLayer) 39 | # thisGlyph.endUndo() # undo grouping causes crashes 40 | 41 | except Exception as e: 42 | Glyphs.showMacroWindow() 43 | print("\n⚠️ Script Error:\n") 44 | import traceback 45 | print(traceback.format_exc()) 46 | print() 47 | raise e 48 | 49 | finally: 50 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 51 | -------------------------------------------------------------------------------- /Anchors/Prefix all exit:entry anchors with a hashtag.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Prefix all exit & entry anchors with a hashtag 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Looks for all exit and entry anchors anywhere in the font, and disables curs feature generation. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | 11 | def processLayer(thisLayer): 12 | foundExitOrEntry = False 13 | for thisAnchor in thisLayer.anchors: 14 | if thisAnchor.name in ("exit", "entry"): 15 | thisAnchor.name = "#%s" % thisAnchor.name 16 | foundExitOrEntry = True 17 | return foundExitOrEntry 18 | 19 | 20 | def processGlyph(thisGlyph): 21 | layerCount = 0 22 | for thisLayer in thisGlyph.layers: 23 | if processLayer(thisLayer): 24 | layerCount += 1 25 | if layerCount: 26 | print("%s: changed anchor names on %i layer%s" % ( 27 | thisGlyph.name, 28 | layerCount, 29 | "" if layerCount == 1 else "s", 30 | )) 31 | return 1 32 | 33 | return 0 34 | 35 | 36 | # brings macro window to front and clears its log: 37 | Glyphs.clearLog() 38 | thisFont = Glyphs.font # frontmost font 39 | 40 | print("Looking for exit/entry in %s:" % thisFont.familyName) 41 | print(thisFont.filepath) 42 | print("Scanning %i glyphs..." % len(thisFont.glyphs)) 43 | print() 44 | glyphCount = 0 45 | for thisGlyph in thisFont.glyphs: 46 | # thisGlyph.beginUndo() # undo grouping causes crashes 47 | glyphCount += processGlyph(thisGlyph) 48 | # thisGlyph.beginUndo() # undo grouping causes crashes 49 | 50 | reportMessage = "Hashtagged exit/entry anchors in %i glyph%s." % ( 51 | glyphCount, 52 | "" if glyphCount == 1 else "s", 53 | ) 54 | 55 | print("\n%s\nDone." % reportMessage) 56 | Message(title="Exit/Entry Prefix Report", message="Font ‘%s’: %s Detailed report in Macro Window." % (thisFont.familyName, reportMessage), OKButton=None) 57 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Compare Kerning Groups of Two Frontmost Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Compare Kerning Groups 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Compares the kerning groups of exporting glyphs in the two frontmost fonts and outputs a report in the Macro Window. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.fonts[0] # frontmost font 11 | otherFont = Glyphs.fonts[1] # second font 12 | 13 | thisGlyphSet = [g.name for g in thisFont.glyphs if g.export] 14 | commonGlyphSet = [g.name for g in otherFont.glyphs if g.export and g.name in thisGlyphSet] 15 | 16 | # brings macro window to front and clears its log: 17 | Glyphs.clearLog() 18 | Glyphs.showMacroWindow() 19 | 20 | print("Differing kerning groups between:") 21 | print(u"1. %s\n %s\n" % (thisFont.familyName, thisFont.filepath)) 22 | print(u"2. %s\n %s\n" % (otherFont.familyName, otherFont.filepath)) 23 | 24 | columnHead = u"◀️ ▶️ GLYPHNAME" 25 | print(columnHead) 26 | print("-" * len(columnHead)) 27 | 28 | sameInBothFonts = [] 29 | differencesBetweenFonts = [] 30 | for glyphName in commonGlyphSet: 31 | thisGlyph = thisFont.glyphs[glyphName] 32 | otherGlyph = otherFont.glyphs[glyphName] 33 | 34 | leftGroupSame = thisGlyph.leftKerningGroup == otherGlyph.leftKerningGroup 35 | rightGroupSame = thisGlyph.rightKerningGroup == otherGlyph.rightKerningGroup 36 | 37 | if leftGroupSame and rightGroupSame: 38 | sameInBothFonts.append(glyphName) 39 | else: 40 | differencesBetweenFonts.append(glyphName) 41 | print(u"%s %s %s" % (u"✅" if leftGroupSame else u"❌", u"✅" if rightGroupSame else u"❌", glyphName)) 42 | 43 | print() 44 | print(u"✅ Glyphs with same goups:") 45 | print("/" + "/".join(sameInBothFonts)) 46 | print() 47 | print(u"❌ Glyphs with different groups:") 48 | print("/" + "/".join(differencesBetweenFonts)) 49 | print() 50 | -------------------------------------------------------------------------------- /Post Production/winfix.py: -------------------------------------------------------------------------------- 1 | """ 2 | winfix.py 3 | Usage: 4 | python3 winfix.py [options] font1.ttf font2.ttf ... 5 | 6 | Options: 7 | -h, --help Show this help message and exit 8 | -u, --usage Show usage information and exit 9 | -f, --force Overwrite existing fvar tables if present 10 | """ 11 | 12 | import sys 13 | from fontTools.ttLib import TTFont 14 | from fontTools.ttLib.tables import _f_v_a_r 15 | 16 | def addEmptyFvar(ttFont, filename, force=False): 17 | if 'fvar' in ttFont and not force: 18 | print(f'⚠️ fvar table already exists in {filename}. Use -f/--force to overwrite.') 19 | return False 20 | # Create a new empty fvar table 21 | fvar = _f_v_a_r.table__f_v_a_r() 22 | fvar.axes = [] 23 | fvar.instances = [] 24 | ttFont['fvar'] = fvar 25 | return True 26 | 27 | def printUsage(): 28 | usageText = """ 29 | winfix.py 30 | Usage: 31 | python3 winfix.py [options] font1.ttf font2.ttf ... 32 | 33 | Options: 34 | -h, --help Show this help message and exit 35 | -u, --usage Show usage information and exit 36 | -f, --force Overwrite existing fvar tables if present 37 | """ 38 | print(usageText) 39 | 40 | def main(): 41 | args = sys.argv[1:] 42 | force = False 43 | if not args or '-h' in args or '--help' in args or '-u' in args or '--usage' in args: 44 | printUsage() 45 | sys.exit(0) 46 | if '-f' in args: 47 | force = True 48 | args.remove('-f') 49 | if '--force' in args: 50 | force = True 51 | args.remove('--force') 52 | 53 | for fontPath in args: 54 | if fontPath.startswith('-'): 55 | continue 56 | try: 57 | font = TTFont(fontPath) 58 | success = addEmptyFvar(font, fontPath, force=force) 59 | if success: 60 | font.save(fontPath) 61 | print(f'✅ Added empty fvar in: {fontPath}') 62 | except Exception as e: 63 | print(f'‼️ Error processing {fontPath}: {e}') 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /Interpolation/Report Instance Interpolations.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Report Instance Interpolations 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Outputs master coefficients for each instance in Macro Window. Tells you which masters are involved in interpolating a specific instance, and to which extent. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | 11 | def reportCoefficients(instance): 12 | interpolations = instance.instanceInterpolations 13 | for masterId in interpolations.allKeys(): 14 | master = Glyphs.font.masters[masterId] 15 | print(" Ⓜ%7.2f%% %s" % (interpolations[masterId] * 100, master.name)) 16 | 17 | 18 | thisFont = Glyphs.font # frontmost font 19 | if thisFont is None: 20 | Message( 21 | title="No Font Open", 22 | message="The script requires a font. Open a font and run the script again.", 23 | OKButton=None, 24 | ) 25 | elif not thisFont.instances: 26 | Message( 27 | title="No Instances Set", 28 | message="This font has no instances set up in File > Font Info > %s. Insert instances and run the script again." % 29 | ("Exports" if Glyphs.versionNumber >= 3 else "Instances", ), 30 | OKButton=None, 31 | ) 32 | else: 33 | # brings macro window to front and clears its log: 34 | Glyphs.clearLog() 35 | Glyphs.showMacroWindow() 36 | print("Instance Report for %s" % thisFont.familyName) 37 | if thisFont.filepath: 38 | print("📄 %s" % thisFont.filepath.lastPathComponent()) 39 | else: 40 | print("⚠️ The font file has not been saved yet.") 41 | 42 | for thisInstance in thisFont.instances: 43 | if Glyphs.buildNumber > 3198: 44 | instanceIsExporting = thisInstance.exports 45 | else: 46 | instanceIsExporting = thisInstance.active 47 | print("\n%s %s %s" % ( 48 | "🟢" if instanceIsExporting else "🚫", 49 | thisInstance.familyName, 50 | thisInstance.name, 51 | )) 52 | reportCoefficients(thisInstance) 53 | -------------------------------------------------------------------------------- /Kerning/New Tab with All Group Members.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with all Group Members 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Select two glyphs, e.g. ‘Ta’ (or place your cursor between them), run the script, it will give you a new tab with all combinations of the ‘T’ kerning group with the ‘a’ kerning group. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | tab = thisFont.currentTab 12 | tabString = "" 13 | listOfSelectedLayers = thisFont.selectedLayers 14 | firstGlyph, secondGlyph = None, None 15 | 16 | if len(listOfSelectedLayers) == 2: 17 | firstGlyph = listOfSelectedLayers[0].parent 18 | secondGlyph = listOfSelectedLayers[1].parent 19 | elif tab and tab.textRange == 0: 20 | cursorPosition = tab.layersCursor 21 | if cursorPosition > 0: 22 | firstGlyph = tab.layers[cursorPosition - 1].parent 23 | secondGlyph = tab.layers[cursorPosition].parent 24 | 25 | if firstGlyph and secondGlyph: 26 | firstGroup = firstGlyph.rightKerningGroup 27 | secondGroup = secondGlyph.leftKerningGroup 28 | 29 | firstGlyphNames = [g.name for g in thisFont.glyphs if g.rightKerningGroup == firstGroup] 30 | secondGlyphNames = [g.name for g in thisFont.glyphs if g.leftKerningGroup == secondGroup] 31 | 32 | for firstGlyphName in firstGlyphNames: 33 | for secondGlyphName in secondGlyphNames: 34 | thisPair = "/%s/%s/space" % (firstGlyphName, secondGlyphName) 35 | tabString += thisPair 36 | tabString = tabString.strip() + "\n" 37 | 38 | tabString = tabString.strip() 39 | if tabString: 40 | # opens new Edit tab: 41 | thisFont.newTab(tabString) 42 | else: 43 | Message( 44 | title="Invalid Glyph Selection", 45 | message="This script needs exactly two glyphs selected, it will use the right kerning group of the left glyph, and vice versa. Select two glyphs and try again.", 46 | OKButton=None 47 | ) 48 | -------------------------------------------------------------------------------- /Spacing/New Tab with Fraction Figure Combinations.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: New Tab with Fraction Figure Combinations 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Open Tab with fraction figure combos for spacing and kerning. Hold down COMMAND and SHIFT for all fonts. 6 | """ 7 | 8 | from AppKit import NSEvent, NSEventModifierFlagShift, NSEventModifierFlagCommand 9 | from GlyphsApp import Glyphs 10 | 11 | keysPressed = NSEvent.modifierFlags() 12 | shiftKeyPressed = keysPressed & NSEventModifierFlagShift == NSEventModifierFlagShift 13 | commandKeyPressed = keysPressed & NSEventModifierFlagCommand == NSEventModifierFlagCommand 14 | 15 | if commandKeyPressed and shiftKeyPressed: 16 | fonts = Glyphs.fonts 17 | else: 18 | fonts = [Glyphs.font, ] 19 | 20 | for thisFont in fonts: 21 | paragraph = "/%s\n" % "/".join([g.name for g in thisFont.glyphs if g.export and (g.name.startswith("percent") or g.name.startswith("perthousand"))]) 22 | 23 | percent = thisFont.glyphs["percent"] 24 | if percent and percent.layers[0].components: 25 | percentParticleNames = list([c.name for c in percent.layers[0].components]) 26 | percentParticleNames.append(percentParticleNames[-1]) 27 | paragraph += "/" + "/".join(percentParticleNames) + "\n" 28 | 29 | z = "/zero.numr" 30 | figs = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] 31 | 32 | for numr in figs: 33 | n = "/%s.numr" % numr 34 | line = z + n + z + n + n + z + z 35 | for dnom in figs: 36 | line += "/zero.numr/%s.numr/fraction/%s.dnom/zero.dnom " % (numr, dnom) 37 | paragraph += line 38 | paragraph += "\n" 39 | 40 | paragraph += "\n" 41 | for glyph in thisFont.glyphs: 42 | slashedName = f"/{glyph.name}" 43 | if glyph.subCategory == "Fraction" and slashedName not in paragraph: 44 | paragraph += slashedName 45 | 46 | # opens new Edit tab: 47 | thisFont.newTab(paragraph.strip()) 48 | -------------------------------------------------------------------------------- /Anchors/Insert #exit and #entry anchors at sidebearings.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Insert #exit and #entry anchors at sidebearings 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Inserts #entry at LSB and #exit at RSB (LTR) or the other way around (RTL) in all masters and special layers of the selected glyphs. Will not overwrite existing anchors unless you hold down OPT+SHIFT. 6 | """ 7 | 8 | from AppKit import NSEvent, NSPoint 9 | from GlyphsApp import Glyphs, GSAnchor, GSRTL 10 | 11 | 12 | def process(thisLayer, overwrite): 13 | anchorInfos = { 14 | "#exit": thisLayer.width if thisLayer.parent.direction != GSRTL else 0.0, 15 | "#entry": 0.0 if thisLayer.parent.direction != GSRTL else thisLayer.width, 16 | } 17 | for anchorName in anchorInfos.keys(): 18 | if not thisLayer.anchors[anchorName] or overwrite: 19 | x = anchorInfos[anchorName] 20 | newAnchor = GSAnchor() 21 | newAnchor.name = anchorName 22 | newAnchor.position = NSPoint(x, 0.0) 23 | thisLayer.anchors.append(newAnchor) 24 | 25 | 26 | thisFont = Glyphs.font # frontmost font 27 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 28 | Glyphs.clearLog() # clears log in Macro window 29 | 30 | keysPressed = NSEvent.modifierFlags() 31 | optionKey = 524288 32 | shiftKey = 131072 33 | optionKeyPressed = keysPressed & optionKey == optionKey 34 | shiftKeyPressed = keysPressed & shiftKey == shiftKey 35 | forceReset = optionKeyPressed and shiftKeyPressed 36 | 37 | try: 38 | for selectedLayer in selectedLayers: 39 | thisGlyph = selectedLayer.parent 40 | print("Processing %s" % thisGlyph.name) 41 | for thisLayer in thisGlyph.layers: 42 | process(thisLayer, forceReset) 43 | except Exception as e: 44 | Glyphs.showMacroWindow() 45 | print("\n⚠️ Error in script: Insert #exit and #entry anchors at sidebearings\n") 46 | import traceback 47 | print(traceback.format_exc()) 48 | print() 49 | raise e 50 | -------------------------------------------------------------------------------- /Interpolation/Other/masterNavigation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division, print_function, unicode_literals 3 | from GlyphsApp import Glyphs, GSControlLayer 4 | 5 | 6 | def showAllMastersOfGlyphInCurrentTab(thisGlyphName): 7 | thisFont = Glyphs.font 8 | thisGlyph = thisFont.glyphs[thisGlyphName] 9 | if thisGlyph: 10 | thisTab = thisFont.currentTab 11 | if not thisTab: 12 | thisTab = thisFont.newTab() 13 | 14 | thisTab.layers = [layer for layer in thisGlyph.layers if layer.isMasterLayer or layer.isSpecialLayer] 15 | # thisTab.textCursor = 0 16 | thisTab.textRange = 0 17 | 18 | 19 | def showAllMastersOfGlyphs(glyphNames, openNewTab=True, avoidDuplicates=True): 20 | if avoidDuplicates: 21 | glyphNamesSet = [] 22 | [glyphNamesSet.append(g) for g in glyphNames if g not in glyphNamesSet] 23 | glyphNames = glyphNamesSet 24 | 25 | thisFont = Glyphs.font 26 | thisTab = thisFont.currentTab 27 | if openNewTab or not thisTab: 28 | thisTab = thisFont.newTab() 29 | thisTab.textRange = 0 30 | 31 | displayLayers = [] 32 | for thisGlyphName in glyphNames: 33 | thisGlyph = thisFont.glyphs[thisGlyphName] 34 | if thisGlyph: 35 | displayLayers += [layer for layer in thisGlyph.layers if layer.isMasterLayer or layer.isSpecialLayer] 36 | displayLayers.append(GSControlLayer.newline()) 37 | 38 | if len(displayLayers) > 1: 39 | displayLayers.pop(-1) # remove last newline 40 | 41 | thisTab.layers = displayLayers 42 | 43 | 44 | def glyphNameForIndexOffset(indexOffset): 45 | thisFont = Glyphs.font # frontmost font 46 | currentLayer = thisFont.selectedLayers[0] 47 | currentGlyph = currentLayer.parent 48 | glyphIndex = currentGlyph.glyphId() 49 | 50 | # open a new tab with the current glyph if opened from Font tab: 51 | if thisFont.currentTab: 52 | glyphIndex += indexOffset 53 | 54 | thisGlyph = thisFont.glyphs[glyphIndex % len(thisFont.glyphs)] 55 | if thisGlyph: 56 | return thisGlyph.name 57 | else: 58 | return None 59 | -------------------------------------------------------------------------------- /Post Production/Add Empty DSIG to Most Recent Variable Font Export.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Empty DSIG (OTVAR) 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Run this after you export a Variable Font (TTF) and it will add an empty DSIG table. Necessary to pass the MyFonts onboarding of OTVAR TTFs. 6 | """ 7 | 8 | from fontTools import ttLib 9 | from AppKit import NSString 10 | from otvarLib import currentOTVarExportPath, otVarFileName 11 | from GlyphsApp import Glyphs, INSTANCETYPEVARIABLE, Message 12 | 13 | 14 | if Glyphs.versionNumber < 3.2: 15 | Message( 16 | title="Version Error", 17 | message="This script requires app version 3.2 or later.", 18 | OKButton=None, 19 | ) 20 | else: 21 | # brings macro window to front and clears its log: 22 | Glyphs.clearLog() 23 | Glyphs.showMacroWindow() 24 | print("Adding DSIG for current font") 25 | 26 | thisFont = Glyphs.font # frontmost font 27 | currentExportPath = currentOTVarExportPath() 28 | print(f"🖥️ Export path: {currentExportPath}") 29 | 30 | variableFontSettings = [] 31 | for instance in thisFont.instances: 32 | if instance.type == INSTANCETYPEVARIABLE: 33 | variableFontSettings.append(instance) 34 | 35 | if not variableFontSettings: 36 | print("⚠️ No VF Settings found in Font Info > Exports.") 37 | else: 38 | for i, variableFontExport in enumerate(variableFontSettings): 39 | print(f"\n⚙️ Variable font setting {i + 1}:") 40 | fontpath = NSString.alloc().initWithString_(currentExportPath).stringByAppendingPathComponent_(otVarFileName(thisFont, thisInstance=variableFontExport)) 41 | print(f"📄 {fontpath}") 42 | font = ttLib.TTFont(file=fontpath) 43 | dsig = ttLib.ttFont.newTable("DSIG") 44 | dsig.ulVersion = 1 45 | dsig.usFlag = 0 46 | dsig.usNumSigs = 0 47 | dsig.signatureRecords = [] 48 | font["DSIG"] = dsig 49 | font.save(fontpath, reorderTables=False) 50 | print("💾 Saved file.") 51 | print("\n✅ Done.") 52 | -------------------------------------------------------------------------------- /Anchors/Add missing smart anchors.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add missing smart anchors 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds all anchors for properties of selected smart glyphs on all their layers. Skips Width and Height anchors. 6 | """ 7 | 8 | from AppKit import NSPoint 9 | from GlyphsApp import Glyphs, GSAnchor 10 | 11 | 12 | def process(glyph): 13 | countOfAnchors = 0 14 | countOfLayers = 0 15 | axisNames = [a.name for a in glyph.smartComponentAxes if a.name not in ("Width", "Height")] 16 | for thisLayer in glyph.layers: 17 | if thisLayer.isSpecialLayer: 18 | countOfLayers += 1 19 | for i, axisName in enumerate(axisNames): 20 | if not thisLayer.anchors[axisName]: 21 | smartAnchor = GSAnchor(axisName, NSPoint(0, -i * 50)) 22 | thisLayer.anchors.append(smartAnchor) 23 | countOfAnchors += 1 24 | print(f" - added {countOfAnchors} anchors on {countOfLayers} smart layers.") 25 | 26 | 27 | thisFont = Glyphs.font # frontmost font 28 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 29 | Glyphs.clearLog() # clears log in Macro window 30 | print(f"Adding missing smart anchors in {len(selectedLayers)} glyphs:\n") 31 | 32 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 33 | try: 34 | for thisLayer in selectedLayers: 35 | thisGlyph = thisLayer.parent 36 | if not thisGlyph.isSmartGlyph(): 37 | print(f"⏭️ Skipping {thisGlyph.name} (not a smart glyph).") 38 | else: 39 | print(f"⚙️ Processing {thisGlyph.name}:") 40 | thisGlyph.beginUndo() # begin undo grouping 41 | process(thisGlyph) 42 | thisGlyph.endUndo() # end undo grouping 43 | except Exception as e: 44 | Glyphs.showMacroWindow() 45 | print("\n⚠️ Error in script: Add missing smart anchors\n") 46 | import traceback 47 | print(traceback.format_exc()) 48 | print() 49 | raise e 50 | finally: 51 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 52 | -------------------------------------------------------------------------------- /Pixelfonts/Reset rotated and mirrored components.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reset Rotated and Mirrored Components 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Looks for mirrored and rotated components and resets them to their original orientation. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font 11 | selectedLayers = thisFont.selectedLayers 12 | grid = thisFont.grid 13 | 14 | # brings macro window to front and clears its log: 15 | Glyphs.clearLog() 16 | print("Fixing rotated components: %s" % thisFont.familyName) 17 | print("Processing %i selected glyph%s:\n" % (len(selectedLayers), "" if len(selectedLayers) == 1 else "s")) 18 | 19 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 20 | try: 21 | for layer in selectedLayers: 22 | thisGlyph = layer.parent 23 | glyphName = thisGlyph.name 24 | # thisGlyph.beginUndo() # undo grouping causes crashes 25 | compCount = 0 26 | for comp in layer.components: 27 | transform = comp.transform # this is computed in Glyphs 3. When dropping support for Glyphs 2, use the position/scale/rotate API 28 | if transform[0] != 1.0 or transform[3] != 1.0: 29 | position = comp.position 30 | if transform[0] < 0: 31 | position.x -= grid 32 | if transform[3] < 0: 33 | position.y -= grid 34 | comp.transform = (1, 0, 0, 1, position.x, position.y) 35 | compCount += 1 36 | if compCount > 0: 37 | print("✅ Fixed %i component%s in %s" % (compCount, "" if compCount == 1 else "s", glyphName)) 38 | else: 39 | print("🆗 No transformed components found: %s" % glyphName) 40 | # thisGlyph.endUndo() # undo grouping causes crashes 41 | print("\nDone.") 42 | 43 | except Exception as e: 44 | Glyphs.showMacroWindow() 45 | print("\n⚠️ Error in script: \n") 46 | import traceback 47 | print(traceback.format_exc()) 48 | print() 49 | raise e 50 | 51 | finally: 52 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 53 | -------------------------------------------------------------------------------- /Components/Auto-align Composites with Incremental Metrics Keys.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Auto-align Composites with Incremental Metrics Keys 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | For the frontmost font, auto-aligns composites where only the first component’s alignment is disabled. Ignores .tf, .tosf and math operators. Will open a tab with affected glyph layers. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | thisFont = Glyphs.font 11 | 12 | tabLayers = [] 13 | for g in thisFont.glyphs: 14 | if ".tf" in g.name or ".tosf" in g.name or g.subCategory == "Math": 15 | continue 16 | # skip where acceptable 17 | for layer in g.layers: 18 | if (layer.isMasterLayer or layer.isSpecialLayer) and layer.components and not layer.paths and layer.components[0].alignment == -1: 19 | firstComp = layer.components[0] 20 | if len(layer.components) > 0: 21 | otherAlignments = [c.alignment == 1 for c in layer.components[1:]] 22 | if not all(otherAlignments): 23 | tabLayers.append(layer) 24 | print(f"⚠️ {g.name}, {layer.name}: subsequent components aligned ({', '.join([c.name for c in layer.components[1:]])})") 25 | continue 26 | left = int(round(firstComp.x)) 27 | if left != 0: 28 | leftIncrement = f"=={left:+}" 29 | layer.leftMetricsKey = leftIncrement 30 | right = int(round(layer.width - firstComp.componentLayer.width - firstComp.x)) 31 | if right != 0: 32 | rightIncrement = f"=={right:+}" 33 | layer.rightMetricsKey = rightIncrement 34 | firstComp.makeEnableAlignment() 35 | print(f"✅ {g.name}, {layer.name}: {leftIncrement} ↔️ {rightIncrement}, width kept at {layer.width}") 36 | tabLayers.append(layer) 37 | if tabLayers: 38 | newTab = thisFont.newTab() 39 | newTab.layers = tabLayers 40 | else: 41 | Message( 42 | "Did not find any composites where the first component was not aligned. (Note: math operators and tabular figures are ignored.)", 43 | title='Nothing to align' 44 | ) 45 | -------------------------------------------------------------------------------- /Spacing/Remove Layer-Specific Metrics Keys.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Remove Layer-Specific Metrics Keys 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes left and right metrics keys specific to layers (==), in all layers of all selected glyphs. Also simplifies glyph metrics keys (i.e., turns "=H" into "H"). 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def FilterLayerKey(Key): 12 | if Key and Key.startswith("==") and Key.find("+") == -1 and Key.find("-") == -1 and Key.find("*") == -1 and Key.find("/") == -1: 13 | Key = Key[2:] 14 | return Key 15 | return None 16 | 17 | 18 | def FilterGlyphKey(Key): 19 | if Key and Key.startswith("=") and Key.find("+") == -1 and Key.find("-") == -1 and Key.find("*") == -1 and Key.find("/") == -1: 20 | Key = Key[1:] 21 | return Key 22 | return None 23 | 24 | 25 | def remove(): 26 | for layer in Glyphs.font.selectedLayers: 27 | glyph = layer.parent 28 | deletedKeys = 0 29 | if glyph: 30 | for layer in glyph.layers: 31 | Key = FilterLayerKey(layer.leftMetricsKey) 32 | if Key is not None: 33 | layer.setLeftMetricsKey_(Key) 34 | deletedKeys += 1 35 | Key = FilterLayerKey(layer.rightMetricsKey) 36 | if Key is not None: 37 | layer.setRightMetricsKey_(Key) 38 | deletedKeys += 1 39 | Key = FilterLayerKey(layer.widthMetricsKey) 40 | if Key is not None: 41 | layer.setWidthMetricsKey_(Key) 42 | deletedKeys += 1 43 | Key = FilterGlyphKey(glyph.leftMetricsKey) 44 | if Key is not None: 45 | glyph.setLeftMetricsKey_(Key) 46 | deletedKeys += 1 47 | Key = FilterGlyphKey(glyph.rightMetricsKey) 48 | if Key is not None: 49 | glyph.setRightMetricsKey_(Key) 50 | deletedKeys += 1 51 | Key = FilterGlyphKey(glyph.widthMetricsKey) 52 | if Key is not None: 53 | glyph.setWidthMetricsKey_(Key) 54 | deletedKeys += 1 55 | 56 | if deletedKeys: 57 | print("Deleted %i metrics keys: %s" % (deletedKeys, glyph.name)) 58 | 59 | 60 | remove() 61 | -------------------------------------------------------------------------------- /Paths/Grid Switcher.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Grid Switcher 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Toggles grid between two gridstep values. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | 13 | class Gridswitch(mekkaObject): 14 | prefDict = { 15 | "grid1": 1, 16 | "grid2": 0, 17 | } 18 | 19 | def __init__(self): 20 | self.gridStep1default = 1 21 | self.gridStep2default = 0 22 | 23 | currentGridStep = Glyphs.font.gridMain() 24 | self.w = vanilla.FloatingWindow((170, 100), "Grid Switcher", autosaveName=self.domain("mainwindow")) 25 | self.w.grid1 = vanilla.EditText((15, 12, 65, 19), "1", sizeStyle='small') 26 | self.w.grid2 = vanilla.EditText((-80, 12, -15, 19), "50", sizeStyle='small') 27 | self.w.currentGridStep = vanilla.TextBox((15, 38, -15, 22), "Current Grid Step: %i" % currentGridStep) 28 | self.w.switchButton = vanilla.Button((15, -22 - 15, -15, -15), "Switch Grid", callback=self.GridOnOffMain) 29 | self.w.setDefaultButton(self.w.switchButton) 30 | 31 | self.w.center() 32 | self.w.open() 33 | self.w.makeKey() 34 | 35 | # Load Settings: 36 | self.LoadPreferences() 37 | 38 | def GridOnOffMain(self, sender): 39 | try: 40 | self.SavePreferences() 41 | 42 | try: 43 | gridStep1 = int(Glyphs.defaults["com.mekkablue.gridswitch.grid1"]) 44 | except: 45 | gridStep1 = self.gridStep1default 46 | self.w.grid1.set(gridStep1) 47 | 48 | try: 49 | gridStep2 = int(Glyphs.defaults["com.mekkablue.gridswitch.grid2"]) 50 | except: 51 | gridStep2 = self.gridStep2default 52 | self.w.grid2.set(gridStep2) 53 | 54 | gridStep = Glyphs.font.gridMain() 55 | if gridStep != gridStep1: 56 | newGridStep = gridStep1 57 | else: 58 | newGridStep = gridStep2 59 | 60 | Glyphs.font.setGridMain_(newGridStep) 61 | self.w.currentGridStep.set("Current Grid Step: %i" % newGridStep) 62 | 63 | except Exception as e: 64 | raise e 65 | 66 | 67 | Gridswitch() 68 | -------------------------------------------------------------------------------- /Paths/Interpolate two paths.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Interpolate two paths 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Select two paths and run this script, it will replace them with their interpolation at 50%. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSGlyph, GSLayer, GSPath, Message, distance 9 | 10 | Font = Glyphs.font 11 | 12 | if not Font: 13 | Message("You need to open a glyph in a font and select two paths for this script.", title='No font open', OKButton="Hachoo") 14 | else: 15 | Layer = Font.selectedLayers[0] 16 | interpolatablePaths = [] 17 | for path in Layer.paths: 18 | if path.selected: 19 | interpolatablePaths.append(path) 20 | 21 | if len(interpolatablePaths) == 2: 22 | path1, path2 = interpolatablePaths 23 | if len(path1.nodes) != len(path2.nodes): 24 | Message("The two paths you selected do not have the same number of points.", title='Incompatible Paths', OKButton="Duh") 25 | else: 26 | if distance(path1.nodes[0].position, path2.nodes[-1].position) < distance(path1.nodes[0].position, path2.nodes[0].position): 27 | path2.reverse() 28 | tempGlyph = GSGlyph() 29 | layerA, layerB = GSLayer(), GSLayer() 30 | layerA.shapes.append(path1.copy()) 31 | layerB.shapes.append(path2.copy()) 32 | tempGlyph.layers = [layerA, layerB] 33 | tempLayer = tempGlyph._interpolateLayers_interpolation_masters_decompose_font_error_( 34 | [layerA, layerB], { 35 | layerA.layerId: 0.5, 36 | layerB.layerId: 0.5, 37 | }, None, False, Font, None 38 | ) 39 | if len(tempLayer.shapes) == 1: 40 | for i in range(len(Layer.shapes) - 1, -1, -1): 41 | if Layer.shapes[i].selected and isinstance(Layer.shapes[i], GSPath): 42 | del Layer.shapes[i] 43 | Layer.shapes.append(tempLayer.paths[0].copy()) 44 | else: 45 | Message("Could not interpolate the two paths.", title='No interpolation possible', OKButton="Darn") 46 | else: 47 | Message("You need to select exactly two paths to interpolate.", title='Wrong number of paths selected', OKButton="Oh well") 48 | -------------------------------------------------------------------------------- /Color Fonts/Cycle CPAL Colors for Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Cycle CPAL Colors for Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Will increase the color index for each CPAL Color Palette layer, or set to 0 if it exceeds the number of available colors. E.g., for three colors, it will cycle indexes like this: 0→1, 1→2, 2→0. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def process(thisGlyph, maxColor): 12 | for layer in thisGlyph.layers: 13 | paletteIndex = layer.attributes["colorPalette"] 14 | if paletteIndex is not None: 15 | paletteIndex += 1 16 | layer.attributes["colorPalette"] = paletteIndex % maxColor 17 | 18 | 19 | thisFont = Glyphs.font # frontmost font 20 | thisFontMaster = thisFont.selectedFontMaster # active master 21 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 22 | Glyphs.clearLog() # clears log in Macro window 23 | 24 | # get the most relevant color palette: 25 | palettes = thisFontMaster.customParameters["Color Palettes"] 26 | if not palettes: 27 | for thisMaster in thisFont.masters: 28 | palettes = thisMaster.customParameters["Color Palettes"] 29 | if palettes: 30 | break 31 | if not palettes: 32 | palettes = thisFont.customParameters["Color Palettes"] 33 | 34 | if palettes: 35 | maxColor = len(palettes[0]) # determine number of colors 36 | 37 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 38 | try: 39 | for thisLayer in selectedLayers: 40 | thisGlyph = thisLayer.parent 41 | print(f"Cycling colors in {thisGlyph.name}") 42 | thisGlyph.beginUndo() # begin undo grouping 43 | process(thisGlyph, maxColor) 44 | thisGlyph.endUndo() # end undo grouping 45 | except Exception as e: 46 | Glyphs.showMacroWindow() 47 | print("\n⚠️ Error in script: Cycle CPAL Colors for Selected Glyphs\n") 48 | import traceback 49 | print(traceback.format_exc()) 50 | print() 51 | raise e 52 | finally: 53 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 54 | -------------------------------------------------------------------------------- /Color Fonts/Delete Non-Color Layers in Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Delete Non-Color Layers in Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Deletes all sublayers in all glyphs that are not of type "Color X" (CPAL/COLR layers). 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | thisFontMaster = thisFont.selectedFontMaster # active master 12 | thisFontMasterID = thisFontMaster.id 13 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 14 | 15 | 16 | def process(thisGlyph): 17 | for i in range(len(thisGlyph.layers))[::-1]: 18 | currentLayer = thisGlyph.layers[i] 19 | if not currentLayer.layerId == thisFontMasterID: # not the master layer 20 | try: 21 | # GLYPHS 3 22 | isColorLayer = currentLayer.isColorPaletteLayer() 23 | except: 24 | # GLYPHS 2 25 | isColorLayer = currentLayer.name.startswith("Color ") 26 | if not isColorLayer: 27 | currentLayerID = currentLayer.layerId 28 | print("%s: removed non-Color layer ‘%s’" % (thisGlyph.name, currentLayer.name)) 29 | try: 30 | # GLYPHS 3 31 | thisGlyph.removeLayerForId_(currentLayerID) 32 | except: 33 | # GLYPHS 2 34 | thisGlyph.removeLayerForKey_(currentLayerID) 35 | 36 | 37 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 38 | try: 39 | Glyphs.clearLog() 40 | print("Removing non-Color layers in %i glyphs:" % len(listOfSelectedLayers)) 41 | for thisLayer in listOfSelectedLayers: 42 | thisGlyph = thisLayer.parent 43 | print("\nProcessing", thisGlyph.name) 44 | # thisGlyph.beginUndo() # undo grouping causes crashes 45 | process(thisGlyph) 46 | # thisGlyph.endUndo() # undo grouping causes crashes 47 | 48 | except Exception as e: 49 | Glyphs.showMacroWindow() 50 | print("\n⚠️ Script Error:\n") 51 | import traceback 52 | print(traceback.format_exc()) 53 | print() 54 | raise e 55 | 56 | finally: 57 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 58 | -------------------------------------------------------------------------------- /App/Set Export Paths to Adobe Fonts Folder.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set Export Paths to Adobe Fonts Folder 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Sets the OpenType font and Variable Font export paths to the Adobe Fonts Folder. 6 | """ 7 | 8 | import subprocess 9 | from GlyphsApp import Glyphs, Message 10 | 11 | Glyphs.clearLog() 12 | 13 | 14 | def executeCommand(command, commandArgs): 15 | commandExecution = subprocess.Popen(command.split(" ") + commandArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 16 | output, error = commandExecution.communicate() 17 | returncode = commandExecution.returncode 18 | return returncode, output, error 19 | 20 | 21 | adobeFontsFolder = "/Library/Application Support/Adobe/Fonts" 22 | # TODO: check if general Library is accessible (W privileges), if not, use ~/Library 23 | 24 | # Make sure the folder actually exists: 25 | pathSegments = adobeFontsFolder.split("/")[1:] 26 | path = "" 27 | for pathPart in pathSegments: 28 | path += "/%s" % pathPart 29 | returncode, output, error = executeCommand("cd", [path]) 30 | if returncode != 0 and "No such file or directory" in error: 31 | print("Creating directory: %s" % path) 32 | returncode, output, error = executeCommand("mkdir", [path]) 33 | if returncode != 0: 34 | print(" Returncode: %s\n Output: %s\n Error: %s" % (returncode, output, error)) 35 | Glyphs.showMacroWindow() 36 | 37 | # Changing the export paths for Glyphs: 38 | settings = ("GXExportPath", "OTFExportPath") 39 | 40 | # Floating notification: 41 | Message( 42 | title="Export Paths Reset Successfully", 43 | message="Static and variable fonts now export to Adobe Fonts Folder. More details in Macro Window.", 44 | OKButton="🥂 Hurrah", 45 | ) 46 | 47 | for setting in settings: 48 | print("Setting %s to %s:" % (setting, adobeFontsFolder), end=' ') 49 | try: 50 | Glyphs.defaults[setting] = adobeFontsFolder 51 | print("OK.") 52 | except Exception as e: 53 | print("\nTRACEBACK:") 54 | import traceback 55 | print(traceback.format_exc()) 56 | print("\nEXCEPTION:") 57 | print(e) 58 | -------------------------------------------------------------------------------- /Features/Floating Features.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Floating Features 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Floating window for activating features in the frontmost Edit tab. 6 | """ 7 | 8 | import vanilla 9 | import traceback 10 | from GlyphsApp import Glyphs 11 | from mekkablue import mekkaObject 12 | 13 | 14 | class FeatureActivator(mekkaObject): 15 | 16 | def __init__(self): 17 | numOfFeatures = len(Glyphs.font.features) 18 | 19 | self.w = vanilla.FloatingWindow((150, 10 + numOfFeatures * 18), "", autosaveName=self.domain("mainwindow")) 20 | i = 0 21 | selectedFeatures = Glyphs.currentDocument.windowController().activeEditViewController().selectedFeatures() 22 | for feature in Glyphs.font.features: 23 | featureTag = feature.name 24 | featureName = feature.fullName() 25 | exec( 26 | "self.w.featureCheckBox_%i = vanilla.CheckBox((8, 4 + 18 * i, -8, 18), featureName, sizeStyle='small', callback=self.toggleFeature, value=(featureTag in selectedFeatures))" 27 | % i 28 | ) 29 | exec("self.w.featureCheckBox_%i.getNSButton().setIdentifier_(featureTag)" % i) 30 | i += 1 31 | self.w.open() 32 | 33 | def toggleFeature(self, sender): 34 | try: 35 | editTab = Glyphs.currentDocument.windowController().activeEditViewController() 36 | featureTag = sender.getNSButton().identifier() 37 | featureActive = bool(sender.get()) 38 | if featureActive: 39 | # add the feature: 40 | try: 41 | editTab.selectedFeatures().append(featureTag) 42 | except: 43 | editTab.selectedFeatures().addObject_(featureTag) 44 | else: 45 | # remove the feature: 46 | try: 47 | while True: 48 | editTab.selectedFeatures().remove(featureTag) 49 | except: 50 | pass 51 | 52 | editTab.graphicView().reflow() 53 | editTab.graphicView().layoutManager().updateActiveLayer() 54 | editTab._updateFeaturePopup() 55 | editTab.updatePreview() 56 | except Exception as e: 57 | print(e) 58 | print(traceback.format_exc()) 59 | return False 60 | 61 | return True 62 | 63 | 64 | FeatureActivator() 65 | -------------------------------------------------------------------------------- /Color Fonts/Reverse CPAL Colors for Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reverse CPAL Colors for Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Will reverse the color indexes for each CPAL Color Palette layer. E.g., for three colors, it will turn indexes 0,1,2 into 2,1,0. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def process(thisGlyph): 12 | indexes = [] 13 | for colorLayer in thisGlyph.layers: 14 | colorIndex = colorLayer.attributeForKey_("colorPalette") 15 | if colorLayer.isSpecialLayer and colorIndex is not None: 16 | indexes.append(colorIndex) 17 | for colorLayer in thisGlyph.layers: 18 | colorIndex = colorLayer.attributeForKey_("colorPalette") 19 | if colorLayer.isSpecialLayer and colorIndex is not None: 20 | colorLayer.setAttribute_forKey_(indexes.pop(), "colorPalette") 21 | 22 | 23 | thisFont = Glyphs.font # frontmost font 24 | thisFontMaster = thisFont.selectedFontMaster # active master 25 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 26 | Glyphs.clearLog() # clears log in Macro window 27 | 28 | # get the most relevant color palette: 29 | palettes = thisFontMaster.customParameters["Color Palettes"] 30 | if not palettes: 31 | for thisMaster in thisFont.masters: 32 | palettes = thisMaster.customParameters["Color Palettes"] 33 | if palettes: 34 | break 35 | if not palettes: 36 | palettes = thisFont.customParameters["Color Palettes"] 37 | 38 | if palettes: 39 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 40 | try: 41 | for thisLayer in selectedLayers: 42 | thisGlyph = thisLayer.parent 43 | print(f"Reversing colors in {thisGlyph.name}") 44 | thisGlyph.beginUndo() # begin undo grouping 45 | process(thisGlyph) 46 | thisGlyph.endUndo() # end undo grouping 47 | except Exception as e: 48 | Glyphs.showMacroWindow() 49 | print("\n⚠️ Error in script: Cycle CPAL Colors for Selected Glyphs\n") 50 | import traceback 51 | print(traceback.format_exc()) 52 | print() 53 | raise e 54 | finally: 55 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 56 | -------------------------------------------------------------------------------- /Anchors/Fix Arabic Anchor Order in Ligatures.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Fix Arabic Anchor Ordering in Ligatures 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Fix the order of top_X and bottom_X anchors to RTL. 6 | """ 7 | 8 | from Foundation import NSPoint 9 | from GlyphsApp import Glyphs, GSAnchor 10 | 11 | 12 | Font = Glyphs.font 13 | FontMaster = Font.selectedFontMaster 14 | selectedLayers = Font.selectedLayers 15 | 16 | 17 | def makeAnchor(thisLayer, anchorName, x, y): 18 | thisAnchorPosition = NSPoint() 19 | thisAnchorPosition.x = x 20 | thisAnchorPosition.y = y 21 | thisAnchorName = anchorName 22 | 23 | thisAnchor = GSAnchor(thisAnchorName, thisAnchorPosition) 24 | thisLayer.addAnchor_(thisAnchor) 25 | 26 | print("-- %s (%.1f, %.1f)" % (thisAnchorName, thisAnchorPosition.x, thisAnchorPosition.y)) 27 | 28 | 29 | def addListOfAnchors(thisLayer, baseName, listOfCoordinates): 30 | for i in range(len(listOfCoordinates)): 31 | currCoord = listOfCoordinates[i] 32 | makeAnchor(thisLayer, "%s_%i" % (baseName, i + 1), currCoord[0], currCoord[1]) 33 | 34 | 35 | def deleteAnchors(listOfAnchors): 36 | for i in range(len(listOfAnchors))[::-1]: 37 | del listOfAnchors[i] 38 | 39 | 40 | def process(thisLayer): 41 | topAnchors = [a for a in thisLayer.anchors if "top_" in a.name] 42 | bottomAnchors = [a for a in thisLayer.anchors if "bottom_" in a.name] 43 | topAnchorCoords = sorted([[a.x, a.y] for a in topAnchors], key=lambda l: l[0], reverse=True) 44 | bottomAnchorCoords = sorted([[a.x, a.y] for a in bottomAnchors], key=lambda l: l[0], reverse=True) 45 | 46 | deleteAnchors(topAnchors) 47 | deleteAnchors(bottomAnchors) 48 | 49 | addListOfAnchors(thisLayer, "top", topAnchorCoords) 50 | addListOfAnchors(thisLayer, "bottom", bottomAnchorCoords) 51 | 52 | 53 | Font.disableUpdateInterface() 54 | try: 55 | for thisLayer in selectedLayers: 56 | thisGlyph = thisLayer.parent 57 | thisGlyphName = thisGlyph.name 58 | if "-ar" in thisGlyphName and "_" in thisGlyphName: 59 | print("Processing", thisGlyphName) 60 | process(thisLayer) 61 | except Exception as e: 62 | raise e 63 | finally: 64 | Font.enableUpdateInterface() 65 | -------------------------------------------------------------------------------- /Color Fonts/Merge All Other Masters in Current Master.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Merge All Other Masters in Current Master 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | For selected glyphs, merges all paths from other masters onto the current master layer. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | thisFont = Glyphs.font # frontmost font 11 | thisFontMaster = thisFont.selectedFontMaster # active master 12 | otherMasterIDs = [m.id for m in thisFont.masters if m is not thisFontMaster] 13 | listOfSelectedLayers = thisFont.selectedLayers # active layers of selected glyphs 14 | 15 | 16 | def process(thisGlyph): 17 | currentLayer = thisGlyph.layers[thisFontMaster.id] 18 | print(currentLayer) 19 | try: 20 | # GLYPHS 3 21 | currentLayer.clear() 22 | except: 23 | # GLYPHS 2 24 | currentLayer.paths = None 25 | currentLayer.hints = None 26 | currentLayer.components = None 27 | 28 | for thisID in otherMasterIDs: 29 | sourceLayer = thisGlyph.layers[thisID] 30 | sourcePaths = sourceLayer.paths 31 | if sourcePaths: 32 | for sourcePath in sourcePaths: 33 | currentLayer.paths.append(sourcePath.copy()) 34 | sourceHints = sourceLayer.hints 35 | if sourceHints: 36 | for sourceHint in sourceLayer.hints: 37 | if sourceHint.isCorner(): 38 | currentLayer.hints.append(sourceHint.copy()) 39 | sourceComponents = sourceLayer.components 40 | if sourceComponents: 41 | for sourceComponent in sourceLayer.components: 42 | currentLayer.components.append(sourceComponent.copy()) 43 | 44 | 45 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 46 | try: 47 | for thisLayer in listOfSelectedLayers: 48 | thisGlyph = thisLayer.parent 49 | print("Processing", thisGlyph.name) 50 | # thisGlyph.beginUndo() # undo grouping causes crashes 51 | process(thisGlyph) 52 | # thisGlyph.endUndo() # undo grouping causes crashes 53 | 54 | except Exception as e: 55 | Glyphs.showMacroWindow() 56 | print("\n⚠️ Script Error:\n") 57 | import traceback 58 | print(traceback.format_exc()) 59 | print() 60 | raise e 61 | 62 | finally: 63 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 64 | -------------------------------------------------------------------------------- /Features/Stylistic Sets/Create ssXX from layer.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Create .ssXX glyph from current layer 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Takes the currently opened layers and creates new glyphs with the first available .ssXX ending. It marks the new glyph blue, and (in a multiple-master file) all other, unprocessed master layers orange. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSGlyph 9 | 10 | thisFont = Glyphs.font 11 | allGlyphNames = [x.name for x in thisFont.glyphs] 12 | selectedLayers = thisFont.selectedLayers 13 | 14 | 15 | def findSuffix(glyphName): 16 | nameIsFree = False 17 | ssNumber = 0 18 | 19 | while nameIsFree is False: 20 | ssNumber += 1 21 | targetSuffix = ".ss%.2d" % ssNumber 22 | targetGlyphName = glyphName + targetSuffix 23 | if allGlyphNames.count(targetGlyphName) == 0: 24 | nameIsFree = True 25 | 26 | return targetSuffix 27 | 28 | 29 | def process(sourceLayer): 30 | # find suffix 31 | sourceGlyph = sourceLayer.parent 32 | sourceGlyphName = sourceGlyph.name 33 | targetSuffix = findSuffix(sourceGlyphName) 34 | 35 | # append suffix, create glyph: 36 | targetGlyphName = sourceGlyphName + targetSuffix 37 | targetGlyph = GSGlyph(targetGlyphName) 38 | thisFont.glyphs.append(targetGlyph) 39 | targetGlyph.setColorIndex_(6) 40 | 41 | # copy original layer into respective master of new glyph: 42 | sourceMasterID = sourceLayer.associatedMasterId 43 | layerCopy = sourceLayer.copy() 44 | targetGlyph.layers[sourceMasterID] = layerCopy 45 | 46 | # copy other master layers into target layers: 47 | for thisMasterID in [m.id for m in thisFont.masters if m.id != sourceMasterID]: 48 | targetGlyph.layers[thisMasterID] = sourceGlyph.layers[thisMasterID].copy() 49 | targetGlyph.layers[thisMasterID].setColorIndex_(1) 50 | 51 | try: 52 | # add new glyph to tab: 53 | if thisFont.currentText: 54 | thisFont.currentText = "%s/%s" % (thisFont.currentText, targetGlyphName) 55 | except Exception as e: 56 | print(e) 57 | import traceback 58 | print(traceback.format_exc()) 59 | 60 | print("Created %s" % targetGlyphName) 61 | 62 | 63 | for thisLayer in selectedLayers: 64 | process(thisLayer) 65 | -------------------------------------------------------------------------------- /Post Production/fixpsnames.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | python3 fixpsname.py -h ... help 4 | python3 fixpsname.py *.ttf ... apply to all TTFs in current dir 5 | """ 6 | 7 | from fontTools import ttLib 8 | from argparse import ArgumentParser 9 | parser = ArgumentParser(description="Fix Italic PS Names in fvar when they have a double ‘Italic’ signifier. Will rework the corresponding name table entries.") 10 | 11 | parser.add_argument( 12 | "fonts", 13 | nargs="+", # one or more font names, e.g. *.otf 14 | metavar="font", 15 | help="Any number of OTF or TTF files.", 16 | ) 17 | 18 | 19 | def fixPSnames(otFont): 20 | anythingChanged = False 21 | nameTable = otFont["name"] 22 | for nameTableEntry in nameTable.names: 23 | nameID = nameTableEntry.nameID 24 | # print("CHECK3 nameID", nameID) # DEBUG 25 | nameValue = nameTableEntry.toStr() 26 | oldName = nameValue 27 | # print("CHECK4 nameID", nameID, nameValue) # DEBUG 28 | if nameID in (4, 6, 17): 29 | for oldParticle in ("Regular Italic", "RegularItalic"): 30 | if oldParticle in nameValue: 31 | nameValue = nameValue.replace(oldParticle, "Italic") 32 | if nameID in (3, 6) or nameID > 255: 33 | oldName = nameValue 34 | if "Italic-" in nameValue and nameValue.count("Italic") > 1: 35 | particles = nameValue.split("-") 36 | for i in range(1, len(particles)): 37 | particles[i] = particles[i].replace("Italic", "").strip() 38 | if len(particles[i]) == 0: 39 | particles[i] = "Regular" 40 | nameValue = "-".join(particles) 41 | if nameValue != oldName: 42 | nameTableEntry.string = nameValue 43 | anythingChanged = True 44 | print(f"✅ Changed ID {nameID}: {oldName} → {nameValue}") 45 | return anythingChanged 46 | 47 | 48 | arguments = parser.parse_args() 49 | fonts = arguments.fonts 50 | changed = 0 51 | for i, fontpath in enumerate(fonts): 52 | print(f"\n📄 {i + 1}. {fontpath}") 53 | font = ttLib.TTFont(fontpath) 54 | changesMade = fixPSnames(font) 55 | if changesMade: 56 | changed += 1 57 | font.save(fontpath, reorderTables=False) 58 | print(f"💾 Saved {fontpath}") 59 | else: 60 | print("🤷🏻‍♀️ No changes made. File left unchanged.") 61 | 62 | print(f"\n✅ Done. Changed {changed} of {i + 1} fonts.\n") 63 | -------------------------------------------------------------------------------- /Post Production/otvarLib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | from GlyphsApp import Glyphs 5 | 6 | 7 | def currentStaticExportPath(): 8 | # GLYPHS 3 9 | exportPath = Glyphs.defaults["OTFExportPathManual"] 10 | if Glyphs.defaults["OTFExportUseExportPath"]: 11 | exportPath = Glyphs.defaults["OTFExportPath"] 12 | return exportPath 13 | 14 | 15 | def currentOTVarExportPath(): 16 | exportPath = Glyphs.defaults["GXExportPathManual"] 17 | if Glyphs.defaults["GXExportUseExportPath"]: 18 | exportPath = Glyphs.defaults["GXExportPath"] 19 | return exportPath 20 | 21 | 22 | def otVarFamilyName(thisFont): 23 | if thisFont.customParameters["Variable Font Family Name"]: 24 | return thisFont.customParameters["Variable Font Family Name"] 25 | else: 26 | return thisFont.familyName 27 | 28 | 29 | def otVarFileName(thisFont, thisInstance=None, suffix="ttf"): 30 | """ 31 | Reconstruct export file name of OTVAR. 32 | """ 33 | if thisInstance is not None: 34 | if Glyphs.versionNumber >= 3.3: 35 | return thisInstance.finalPath_error_(suffix, None) 36 | fileName = thisInstance.fileName() 37 | # circumvent bug in Glyphs 3.0.5 38 | if fileName.endswith(".otf"): 39 | fileName = fileName[:-4] 40 | if not fileName: 41 | fileName = thisInstance.customParameters["fileName"] 42 | if not fileName: 43 | familyName = familyNameOfInstance(thisInstance) 44 | fileName = f"{familyName}-{thisInstance.name}".replace(" ", "") 45 | return f"{fileName}.{suffix}" 46 | elif thisFont.customParameters["Variable Font File Name"] or thisFont.customParameters["variableFileName"]: 47 | fileName = thisFont.customParameters["Variable Font File Name"] 48 | if not fileName: 49 | fileName = thisFont.customParameters["variableFileName"] 50 | return f"{fileName}.{suffix}" 51 | else: 52 | familyName = otVarFamilyName(thisFont) 53 | fileName = f"{familyName}VF.{suffix}" 54 | fileName = fileName.replace(" ", "") 55 | return fileName 56 | 57 | 58 | def familyNameOfInstance(thisInstance): 59 | familyNameProperty = thisInstance.propertyForName_languageTag_("familyNames", "dflt") 60 | if familyNameProperty: 61 | return familyNameProperty.value 62 | else: 63 | return thisInstance.font.familyName 64 | -------------------------------------------------------------------------------- /Anchors/Insert exit and entry Anchors to Selected Positional Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Insert exit and entry Anchors to Selected Positional Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds exit and entry anchors to (Arabic) init, medi and fina glyphs, for cursive attachment. 6 | """ 7 | 8 | from Foundation import NSPoint 9 | from GlyphsApp import Glyphs, GSAnchor, GSPath, GSOFFCURVE 10 | 11 | 12 | Glyphs.clearLog() 13 | Glyphs.showMacroWindow() 14 | 15 | Font = Glyphs.font 16 | selectedLayers = Font.selectedLayers 17 | 18 | 19 | def findOncurveAtRSB(thisLayer): 20 | 21 | try: 22 | paths = [p for p in thisLayer.shapes if isinstance(p, GSPath)] 23 | except: 24 | paths = thisLayer.paths 25 | 26 | rightMostPoint = None 27 | for thisPath in paths: 28 | print(thisPath) 29 | for thisNode in thisPath.nodes: 30 | print(thisNode.x) 31 | if thisNode.type != GSOFFCURVE and (rightMostPoint is None or rightMostPoint.x < thisNode.x): 32 | rightMostPoint = thisNode 33 | 34 | if rightMostPoint: 35 | return rightMostPoint 36 | else: 37 | print("%s: No potential entry point" % (thisLayer.parent.name)) 38 | return None 39 | 40 | 41 | def process(thisLayer): 42 | listOfAnchorNames = [a.name for a in thisLayer.anchors] 43 | glyphName = thisLayer.parent.name 44 | 45 | # add exit at 0,0: 46 | if ".init" in glyphName or ".medi" in glyphName: 47 | if len(thisLayer.paths) > 0: 48 | if "exit" not in listOfAnchorNames: 49 | myExit = GSAnchor("exit", NSPoint(0.0, 0.0)) 50 | thisLayer.addAnchor_(myExit) 51 | print("%s: exit" % glyphName) 52 | 53 | # add entry at RSB: 54 | if ".medi" in glyphName or ".fina" in glyphName: 55 | if "entry" not in listOfAnchorNames: 56 | myEntryPoint = findOncurveAtRSB(thisLayer) 57 | if myEntryPoint is not None: 58 | myEntry = GSAnchor("entry", NSPoint(myEntryPoint.x, myEntryPoint.y)) 59 | thisLayer.anchors.append(myEntry) 60 | print("%s: entry" % glyphName) 61 | 62 | 63 | Font.disableUpdateInterface() 64 | try: 65 | for thisLayer in selectedLayers: 66 | thisGlyph = thisLayer.parent 67 | process(thisLayer) 68 | except Exception as e: 69 | raise e 70 | finally: 71 | Font.enableUpdateInterface() 72 | -------------------------------------------------------------------------------- /Features/Stylistic Sets/Synchronize ssXX glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Synchronize ssXX glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates missing ssXX glyphs so that you have synchronous groups of ssXX glyphs. 6 | E.g. you have a.ss01 b.ss01 c.ss01 a.ss02 c.ss02 --> the script creates b.ss02 7 | """ 8 | 9 | from GlyphsApp import Glyphs 10 | 11 | Font = Glyphs.font 12 | allGlyphs = [x.name for x in list(Font.glyphs)] 13 | 14 | 15 | def ssXXsuffix(i): 16 | """Turns an integer into an ssXX ending between .ss01 and .ss20, e.g. 5 -> '.ss05'.""" 17 | if i < 1: 18 | i = 1 19 | elif i > 20: 20 | i = 20 21 | return ".ss%0.2d" % i 22 | 23 | 24 | def stripsuffix(glyphname): 25 | """Returns the glyphname without the dot suffix.""" 26 | dotindex = glyphname.find(".") 27 | return glyphname[:dotindex] 28 | 29 | 30 | i = 1 31 | ssXX_exists = True 32 | 33 | while ssXX_exists: 34 | ssXX = ssXXsuffix(i) 35 | ssXXglyphs = [x for x in allGlyphs if x.find(ssXX) is not -1] 36 | if len(ssXXglyphs) == 0: 37 | i -= 1 38 | ssXX = ssXXsuffix(i) 39 | ssXXglyphs = [x for x in allGlyphs if x.find(ssXX) is not -1] 40 | ssXX_exists = False 41 | else: 42 | i += 1 43 | 44 | if i == 0: 45 | print("No ssXX glyphs in the font. Aborting.") 46 | else: 47 | print("Highest ssXX:", ssXX) 48 | print("Creating:") 49 | for XX in range(i): 50 | 51 | ssXXglyphs = [x for x in allGlyphs if x.find(ssXXsuffix(XX + 1)) is not -1] 52 | baseglyphs = [stripsuffix(x) for x in ssXXglyphs] 53 | 54 | for YY in range(i): 55 | if XX != YY: 56 | allGlyphs = [x.name for x in list(Font.glyphs)] # neu holen, Glyphen haben sich u.U. schon geaendert 57 | 58 | for thisglyphname in baseglyphs: 59 | targetglyphname = thisglyphname + ssXXsuffix(YY + 1) 60 | 61 | if targetglyphname not in allGlyphs: 62 | sourceglyphname = thisglyphname + ssXXsuffix(XX + 1) 63 | sourceglyph = Font.glyphs[sourceglyphname] 64 | targetglyph = sourceglyph.copy() 65 | targetglyph.name = targetglyphname 66 | targetglyph.unicode = None 67 | targetglyph.productionName = Glyphs.productionGlyphName(targetglyphname) 68 | Font.glyphs.append(targetglyph) 69 | print("- %s" % targetglyphname) 70 | 71 | print() 72 | -------------------------------------------------------------------------------- /Pixelfonts/Align Anchors to Grid.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Align Anchors to Grid 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Looks for anchors not on the grid and rounds their coordinate to the closest grid. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def process(thisLayer): 12 | count = 0 13 | if len(thisLayer.anchors) != 0: 14 | # thisLayer.parent.beginUndo() # undo grouping causes crashes 15 | 16 | for a in thisLayer.anchors: 17 | xrest = a.x % pixelwidth 18 | yrest = a.y % pixelwidth 19 | if xrest or yrest: 20 | # round: 21 | oldX = a.x 22 | oldY = a.y 23 | a.x = round(a.x / pixelwidth) * pixelwidth 24 | a.y = round(a.y / pixelwidth) * pixelwidth 25 | 26 | # report: 27 | count += 1 28 | if count == 1: 29 | reportGlyphName = "%s" % thisLayer.parent.name 30 | elif count == 2: 31 | reportGlyphName = " " * len(reportGlyphName) # indent 32 | print("%s ⚓️ %s %i,%i → %i,%i" % (reportGlyphName, a.name, int(oldX), int(oldY), int(a.x), int(a.y))) 33 | 34 | # thisLayer.parent.endUndo() # undo grouping causes crashes 35 | return count 36 | 37 | 38 | thisFont = Glyphs.font 39 | thisFont.disableUpdateInterface() 40 | try: 41 | selectedLayers = thisFont.selectedLayers 42 | pixelwidth = thisFont.gridLength 43 | 44 | # report: 45 | Glyphs.clearLog() 46 | print("Aligning anchors to grid in font: %s" % thisFont.familyName) 47 | print("Processing: %i glyph%s" % (len(selectedLayers), "" if len(selectedLayers) == 1 else "s")) 48 | print("Grid: %i\n" % pixelwidth) 49 | 50 | # process and keep count: 51 | anchorCount = 0 52 | for thisLayer in selectedLayers: 53 | anchorCount += process(thisLayer) 54 | 55 | # report: 56 | print("\nMoved %i anchors. Done." % anchorCount) 57 | Glyphs.showNotification( 58 | "Grid-aligned anchors in %s" % (thisFont.familyName), 59 | "Aligned %i anchors in %i selected glyphs. Details in Macro Window." % (anchorCount, len(selectedLayers)), 60 | ) 61 | except Exception as e: 62 | Glyphs.showMacroWindow() 63 | print("\n⚠️ Script Error:\n") 64 | import traceback 65 | print(traceback.format_exc()) 66 | print() 67 | raise e 68 | finally: 69 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 70 | -------------------------------------------------------------------------------- /Color Fonts/Add All Color Layers to Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add All Missing Color Layers to Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds a duplicate of the fallback (master) layer for each color defined in the Color Palettes parameter, for each selected glyph. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | 11 | def glyphAlreadyHasLayerForThisColor(glyph, mID, colorIndex): 12 | for layer in glyph.layers: 13 | if layer.associatedMasterId == mID: 14 | try: 15 | # GLYPHS 3 16 | if layer.isColorPaletteLayer() and layer.attributeForKey_("colorPalette") == colorIndex: 17 | return True 18 | except: 19 | # GLYPHS 2 20 | if layer.name == "Color %i" % colorIndex: 21 | return True 22 | return False 23 | 24 | 25 | def process(thisGlyph, mID, paletteSize): 26 | for i in range(paletteSize): 27 | if glyphAlreadyHasLayerForThisColor(thisGlyph, mID, i): 28 | print("⚠️ Skipping Color %i: already exists" % i) 29 | else: 30 | newLayer = thisGlyph.layers[mID].copy() 31 | try: 32 | # GLYPHS 3 33 | newLayer.setColorPaletteLayer_(1) 34 | newLayer.setAttribute_forKey_(i, "colorPalette") 35 | except: 36 | # GLYPHS 2 37 | newLayer.name = "Color %i" % i 38 | thisGlyph.layers.append(newLayer) 39 | print("✅ Added: Color %i" % i) 40 | 41 | 42 | thisFont = Glyphs.font # frontmost font 43 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 44 | selectedGlyphs = [layer.parent for layer in selectedLayers] 45 | Glyphs.clearLog() # clears log in Macro window 46 | 47 | CPAL = None 48 | parameterName = "Color Palettes" 49 | 50 | for m in thisFont.masters: 51 | mID = m.id 52 | CPAL = m.customParameters[parameterName] 53 | 54 | if not CPAL: 55 | CPAL = thisFont.customParameters[parameterName] 56 | 57 | if CPAL: 58 | paletteSize = len(CPAL[0]) 59 | for thisGlyph in selectedGlyphs: 60 | print("\n🔠 %s" % thisGlyph.name) 61 | process(thisGlyph, mID, paletteSize) 62 | 63 | if CPAL is None: 64 | Message( 65 | title="No Palette Found", 66 | message="No ‘Color Palettes’ parameter found in Font Info > Font or Font Info > Masters. Please add the parameter and try again.", 67 | OKButton=None, 68 | ) 69 | -------------------------------------------------------------------------------- /Anchors/Replicate Anchors in Suffixed Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Replicate Anchors in Suffixed Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Goes through your selected, dot-suffixed glyphs and copies the anchors and anchor positions of the base glyph into them. E.g. A.ss01 will have the same anchor positions as A. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, Message 9 | 10 | 11 | def process(thisLayer): 12 | glyphName = thisLayer.parent.name 13 | baseGlyphName = glyphName[:glyphName.find(".")] 14 | baseGlyph = Font.glyphs[baseGlyphName] 15 | baseLayer = baseGlyph.layers[thisLayer.master.id] 16 | 17 | print("Layer: %s, replicating from %s" % (thisLayer.name, baseGlyphName)) 18 | 19 | anchorCount = 0 20 | for baseAnchor in baseLayer.anchors: 21 | thisAnchor = baseAnchor.copy() 22 | thisLayer.anchors[thisAnchor.name] = thisAnchor 23 | anchorCount += 1 24 | print(" Added %s at %i, %i" % (thisAnchor.name, thisAnchor.x, thisAnchor.y)) 25 | 26 | return anchorCount 27 | 28 | 29 | Font = Glyphs.font 30 | selectedGlyphs = [layer.parent for layer in Font.selectedLayers if "." in layer.parent.name] 31 | 32 | Glyphs.clearLog() # clears macro window log 33 | print("Replicate Anchors in Suffixed Glyphs\nFont: ‘%s’" % Font.familyName) 34 | print("%i glyph%s selected, %i of which suffixed." % ( 35 | len(Font.selectedLayers), 36 | "" if len(Font.selectedLayers) == 1 else "s", 37 | len(selectedGlyphs), 38 | )) 39 | 40 | anchorCount, layerCount = 0, 0 41 | for thisGlyph in selectedGlyphs: 42 | print("\n%s" % thisGlyph.name) 43 | # thisGlyph.beginUndo() # undo grouping causes crashes 44 | for thisLayer in thisGlyph.layers: 45 | if thisLayer.isMasterLayer or thisLayer.isSpecialLayer: 46 | layerCount += 1 47 | anchorCount += process(thisLayer) 48 | # thisGlyph.endUndo() # undo grouping causes crashes 49 | 50 | print("\nDone.") 51 | 52 | Message( 53 | title="Replicate Anchors Report", 54 | message="Set or reset %i anchor%s on %i layer%s of %i glyph%s. Detailed report in Macro Window." % ( 55 | anchorCount, 56 | "" if anchorCount == 1 else "s", 57 | layerCount, 58 | "" if layerCount == 1 else "s", 59 | len(selectedGlyphs), 60 | "" if len(selectedGlyphs) == 1 else "s", 61 | ), 62 | OKButton=None, 63 | ) 64 | -------------------------------------------------------------------------------- /Anchors/Insert #exit and #entry on baseline at selected points.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Insert #exit and #entry on baseline at selected points 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Use the outermost selected points, take their x coordinates, and add #exit and #entry anchors on the baseline with the same x coordinates. Useful for building ligatures from components. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSAnchor, GSNode, GSRTL 9 | 10 | 11 | def process(thisLayer): 12 | isRTL = thisLayer.parent.direction == GSRTL 13 | 14 | def addAnchor(name, x): 15 | thisLayer.anchors.append(GSAnchor(name, (x, 0))) 16 | 17 | bounds = thisLayer.bounds 18 | threshold = bounds.origin.x + bounds.size.width / 2 19 | selectedNodes = [] 20 | for item in thisLayer.selection: 21 | if isinstance(item, GSNode): 22 | selectedNodes.append(item) 23 | selectedNodes = sorted(selectedNodes, key=lambda node: node.x) 24 | if not selectedNodes: 25 | return 26 | if not isRTL: 27 | if selectedNodes[0].x < threshold: 28 | addAnchor("#entry", selectedNodes[0].x) 29 | if selectedNodes[-1].x > threshold: 30 | addAnchor("#exit", selectedNodes[0].x) 31 | else: 32 | if selectedNodes[0].x < threshold: 33 | addAnchor("#exit", selectedNodes[0].x) 34 | if selectedNodes[-1].x > threshold: 35 | addAnchor("#entry", selectedNodes[0].x) 36 | 37 | 38 | thisFont = Glyphs.font # frontmost font 39 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 40 | Glyphs.clearLog() # clears log in Macro window 41 | 42 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 43 | try: 44 | for thisLayer in selectedLayers: 45 | thisGlyph = thisLayer.parent 46 | print(f"Processing {thisGlyph.name}") 47 | thisGlyph.beginUndo() # begin undo grouping 48 | for thatLayer in thisGlyph.layers: 49 | if thatLayer.isMasterLayer or thatLayer.isSpecialLayer: 50 | process(thatLayer) 51 | thisGlyph.endUndo() # end undo grouping 52 | except Exception as e: 53 | Glyphs.showMacroWindow() 54 | print("\n⚠️ Error in script: Add #exit/#entry on baseline at selected points\n") 55 | import traceback 56 | print(traceback.format_exc()) 57 | print() 58 | raise e 59 | finally: 60 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 61 | -------------------------------------------------------------------------------- /Hinting/BlueFuzzer.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: BlueFuzzer 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Extends all alignment zones (except for the baseline zone that should stay at 0). 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | windowHeight = 110 13 | 14 | 15 | class BlueFuzzer(mekkaObject): 16 | prefDict = { 17 | "fuzzValue": 1, 18 | "allMasters": True, 19 | } 20 | 21 | def __init__(self): 22 | self.w = vanilla.FloatingWindow( 23 | (300, windowHeight), "BlueFuzzer", minSize=(250, windowHeight), maxSize=(500, windowHeight), autosaveName=self.domain("mainwindow") 24 | ) 25 | 26 | self.w.text_1 = vanilla.TextBox((15, 12 + 2, 120, 18), "Extend zones by", sizeStyle='small') 27 | self.w.fuzzValue = vanilla.EditText((120, 12, -15, 18), "1", sizeStyle='small') 28 | self.w.allMasters = vanilla.CheckBox((15, 35, -15, 20), "Apply to all masters", value=True, callback=self.SavePreferences, sizeStyle='small') 29 | 30 | self.w.runButton = vanilla.Button((-80 - 15, -20 - 15, -15, -15), "Fuzz", callback=self.BlueFuzzerMain) 31 | self.w.setDefaultButton(self.w.runButton) 32 | 33 | self.LoadPreferences() 34 | self.w.open() 35 | 36 | def BlueFuzzerMain(self, sender): 37 | try: 38 | Font = Glyphs.font 39 | 40 | fuzzValue = int(self.w.fuzzValue.get()) 41 | allMasters = bool(self.w.allMasters.get()) 42 | 43 | if allMasters: 44 | masterList = Font.masters 45 | else: 46 | masterList = [Font.selectedFontMaster] 47 | 48 | for m in masterList: 49 | numOfZones = len(m.alignmentZones) 50 | for i in range(numOfZones): 51 | thisZone = m.alignmentZones[i] 52 | factor = 1 53 | if thisZone.size < 0: # negative zone 54 | factor = -1 55 | if thisZone.position == 0 and factor == -1: # baseline zone must stay where it is 56 | thisZone.setSize_(thisZone.size + fuzzValue * factor) 57 | else: 58 | thisZone.setPosition_(thisZone.position - fuzzValue * factor) 59 | thisZone.setSize_(thisZone.size + (fuzzValue * 2) * factor) 60 | 61 | self.SavePreferences() 62 | self.w.close() 63 | Font.parent.windowController().showFontInfoWindowWithTabSelected_(1) # master tab index 64 | except Exception as e: 65 | raise e 66 | 67 | 68 | BlueFuzzer() 69 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/Report Missing Glyphs in All Open Fonts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Report Missing Glyphs for all Open Fonts 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | In Macro window, reports all glyphs that are missing in some of the currently open files, but present in other other fonts. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | # brings macro window to front and clears its log: 11 | Glyphs.clearLog() 12 | Glyphs.showMacroWindow() 13 | 14 | 15 | def reportName(font): 16 | if font.filepath: 17 | return font.filepath.lastPathComponent().stringByDeletingDotSuffix() 18 | else: 19 | return font.familyName 20 | 21 | 22 | def findMissingItems(listOfLists): 23 | # Create a set to store the items present in each list 24 | # commonItems = listOfLists[0][:] 25 | allItems = listOfLists[0][:] 26 | 27 | # Iterate through the rest of the lists 28 | for subList in listOfLists[1:]: 29 | # commonItems = list([x for x in commonItems if x in subList]) 30 | allItems.extend([x for x in subList if x not in allItems]) 31 | 32 | # Iterate through each list and find missing items 33 | missingItems = {} 34 | for idx, subList in enumerate(listOfLists): 35 | for item in allItems: 36 | if item not in subList: 37 | if item not in missingItems.keys(): 38 | missingItems[item] = [] 39 | missingItems[item].append(idx) 40 | 41 | return missingItems 42 | 43 | 44 | def groupKeysByValues(inputDict): 45 | groupedKeys = {} 46 | 47 | for key, value in inputDict.items(): 48 | valueStr = ", ".join([str(v) for v in value]) 49 | if valueStr not in groupedKeys.keys(): 50 | groupedKeys[valueStr] = [] 51 | groupedKeys[valueStr].append(key) 52 | 53 | return groupedKeys 54 | 55 | 56 | glyphSets = [] 57 | fonts = Glyphs.fonts 58 | for font in fonts: 59 | glyphSet = list([g.name for g in font.glyphs if g.export]) 60 | glyphSets.append(glyphSet) 61 | 62 | missingGlyphsByGlyphs = findMissingItems(glyphSets) 63 | missingGlyphs = groupKeysByValues(missingGlyphsByGlyphs) 64 | 65 | for indicesStr, glyphNames in missingGlyphs.items(): 66 | 67 | indices = [int(i.strip()) for i in indicesStr.split(",")] 68 | 69 | print(f"{', '.join(glyphNames)} missing in:") 70 | fontNames = [reportName(fonts[i]) for i in indices] 71 | report = '\n '.join(fontNames) 72 | print(f" {report}\n") 73 | -------------------------------------------------------------------------------- /Compare Frontmost Fonts/compare.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | from GlyphsApp import Glyphs, GSCustomParameter 5 | 6 | 7 | def compareLists(thisSet, otherSet, ignoreEmpty=False): 8 | for i in range(len(thisSet))[::-1]: 9 | if thisSet[i] in otherSet: 10 | otherSet.remove(thisSet.pop(i)) 11 | elif ignoreEmpty: 12 | if not thisSet[i]: 13 | thisSet.pop(i) 14 | 15 | for i in range(len(otherSet))[::-1]: 16 | if otherSet[i] in thisSet: 17 | thisSet.remove(otherSet.pop(i)) 18 | elif ignoreEmpty: 19 | if not otherSet[i]: 20 | otherSet.pop(i) 21 | 22 | return thisSet, otherSet 23 | 24 | 25 | def cleanUpAndShortenParameterContent(thisParameter, maxLength=20): 26 | if Glyphs.versionNumber >= 3: 27 | # GLYPHS 3 code: 28 | if isinstance(thisParameter, GSCustomParameter): 29 | parameterContent = repr(thisParameter.value) 30 | else: 31 | parameterContent = repr(thisParameter) 32 | else: 33 | # GLYPHS 2 code: 34 | parameterContent = unicode(repr(thisParameter)) # noqa: F821 35 | if len(parameterContent) > maxLength: 36 | parameterContent = u"%s..." % parameterContent[:maxLength].replace(u"\n", u" ") 37 | while " " in parameterContent: 38 | parameterContent = parameterContent.replace(" ", " ") 39 | return parameterContent 40 | 41 | 42 | def compareCount(things, thisCount, otherCount, thisName, otherName): 43 | if thisCount != otherCount: 44 | print(u"❌ Different number of %s:" % things.upper()) 45 | print(u" A. %i %s in %s" % (thisCount, things.lower(), thisName)) 46 | print(u" B. %i %s in %s" % (otherCount, things.lower(), otherName)) 47 | else: 48 | print(u"✅ Same number of %s: %i." % (things.lower(), thisCount)) 49 | 50 | 51 | def lineReport(thisSet, otherSet, thisFileName, otherFileName, name, commaSeparated=False): 52 | if thisSet or otherSet: 53 | if commaSeparated: 54 | separator = ", " 55 | element = "Elements" 56 | else: 57 | separator = "\n " 58 | element = "Code lines" 59 | 60 | if otherSet: 61 | print() 62 | print(u"⚠️ %s not in %s of %s:" % (element, name, thisFileName)) 63 | print(separator.join(otherSet)) 64 | print() 65 | if thisSet: 66 | print() 67 | print(u"⚠️ %s not in %s of %s:" % (element, name, otherFileName)) 68 | print(separator.join(thisSet)) 69 | print() 70 | else: 71 | print(u"💚 %s: same code in both fonts." % name) 72 | -------------------------------------------------------------------------------- /Images/Adjust Image Alpha.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Adjust Image Alpha 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Slider for setting the alpha of all images in selected glyphs. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | 13 | class AdjustImageAlpha(mekkaObject): 14 | prefDict = { 15 | "alphaSlider": 100.0, 16 | } 17 | 18 | def __init__(self): 19 | # Window 'self.w': 20 | windowWidth = 350 21 | windowHeight = 60 22 | windowWidthResize = 500 # user can resize width by this value 23 | windowHeightResize = 0 # user can resize height by this value 24 | self.w = vanilla.FloatingWindow( 25 | (windowWidth, windowHeight), # default window size 26 | "Adjust Image Alpha for Selected Glyphs", # window title 27 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 28 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 29 | autosaveName=self.domain("mainwindow") # stores last window position and size 30 | ) 31 | 32 | # UI elements: 33 | self.w.text_1 = vanilla.TextBox((15 - 1, 12 + 2, 75, 14), "Image Alpha:", sizeStyle='small') 34 | self.w.alphaSlider = vanilla.Slider((100, 12, -55, 19), value=100.0, minValue=10.0, maxValue=100.0, sizeStyle='small', callback=self.AdjustImageAlphaMain) 35 | self.w.indicator = vanilla.TextBox((-50, 12 + 2, -15, 14), "100.0", sizeStyle='small') 36 | 37 | # Load Settings: 38 | self.LoadPreferences() 39 | 40 | # Open window and focus on it: 41 | self.w.open() 42 | self.w.makeKey() 43 | 44 | def AdjustImageAlphaMain(self, sender): 45 | try: 46 | self.SavePreferences() 47 | 48 | self.w.indicator.set("%.1f" % self.prefFloat("alphaSlider")) 49 | thisFont = Glyphs.font # frontmost font 50 | listOfSelectedLayers = thisFont.selectedLayers # active layers of currently selected glyphs 51 | for thisLayer in listOfSelectedLayers: # loop through layers 52 | if thisLayer.backgroundImage: 53 | thisLayer.backgroundImage.alpha = self.prefFloat("alphaSlider") 54 | 55 | except Exception as e: 56 | # brings macro window to front and reports error: 57 | Glyphs.showMacroWindow() 58 | print("Adjust Image Alpha Error: %s" % e) 59 | import traceback 60 | print(traceback.format_exc()) 61 | 62 | 63 | AdjustImageAlpha() 64 | -------------------------------------------------------------------------------- /Build Glyphs/Build Q from O and _tail.Q.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Build Q from O and _tail.Q 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Run this script AFTER doing Component from Selection on the Q tail and naming it _tail.Q. 6 | """ 7 | 8 | from copy import copy as copy 9 | from GlyphsApp import Glyphs, GSAnchor, GSComponent, Message 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | Oglyph = thisFont.glyphs["O"] 13 | Qglyph = thisFont.glyphs["Q"] 14 | tailGlyph = thisFont.glyphs["_tail.Q"] 15 | anchorName = "bottom" 16 | newAnchorName = "_" + anchorName 17 | 18 | if not Oglyph or not Qglyph or not tailGlyph: 19 | Message(title="Missing glyphs", message="For this script, you need ‘O’, ‘Q’ and ‘_tail.Q’ in your font.", OKButton=None) 20 | else: 21 | # del thisFont.glyphs["Q"] 22 | # thisFont.newGlyphWithName_("O+_tail.Q=Q") 23 | for thisLayer in tailGlyph.layers: 24 | thisMaster = thisLayer.associatedFontMaster() 25 | masterID = thisMaster.id 26 | 27 | # if necessary, add anchors to _tail.Q 28 | if not thisLayer.anchors[newAnchorName]: 29 | defaultPosition = Oglyph.layers[masterID].anchors[anchorName].position 30 | newAnchor = GSAnchor(newAnchorName, defaultPosition) 31 | thisLayer.anchors.append(newAnchor) 32 | 33 | # rebuild Q from O: 34 | targetLayer = Qglyph.layers[masterID] 35 | targetLayer.clear() 36 | for componentName in (Oglyph.name, tailGlyph.name): 37 | newComponent = GSComponent(componentName) 38 | newComponent.automaticAlignment = True 39 | targetLayer.components.append(newComponent) 40 | 41 | # add special layers to _tail.Q: 42 | for layerIndex in range(len(Qglyph.layers) - 1, -1, -1): 43 | thisLayer = Qglyph.layers[layerIndex] 44 | if not thisLayer.isMasterLayer and thisLayer.isSpecialLayer: 45 | tailLayer = copy(thisLayer) 46 | 47 | # remove (O) components: 48 | for i in range(len(tailLayer.shapes) - 1, -1, -1): 49 | if isinstance(tailLayer.shapes[i], GSComponent): 50 | del tailLayer.shapes[i] 51 | 52 | # add anchor: 53 | newAnchor = GSAnchor() 54 | newAnchor.name = newAnchorName 55 | newAnchor.position = Oglyph.layers[thisLayer.associatedMasterId].anchors[anchorName].position 56 | tailLayer.anchors.append(newAnchor) 57 | 58 | # add layer to _tail.Q: 59 | tailGlyph.layers.append(tailLayer) 60 | 61 | # remove layer from Q: 62 | del Qglyph.layers[layerIndex] 63 | -------------------------------------------------------------------------------- /Components/Make Glyph Smart.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Make Glyph Smart 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__=""" 5 | Turn the currently selected glyph(s) into smart glyphs, and establish the current font axes as the glyph’s smart axes. 6 | """ 7 | 8 | from GlyphsApp import GSSmartComponentAxis 9 | 10 | def minMaxForLayer(layer, fontAxisID): 11 | # collect all values for this axis: 12 | glyph = layer.parent 13 | font = glyph.parent 14 | axisIndex = -1 15 | for i, axis in enumerate(font.axes): 16 | if axis.id == fontAxisID: 17 | axisIndex = i 18 | break 19 | if axisIndex < 0: 20 | return 0 # neither max nor min 21 | axisValues = [] 22 | for master in font.masters: 23 | axisValues.append(master.axes[axisIndex]) 24 | 25 | # is the current layer min or max? 26 | currentMaster = layer.associatedFontMaster() 27 | currentValue = currentMaster.axes[fontAxisID] 28 | if currentValue == max(axisValues): 29 | return 2 # max 30 | elif currentValue == min(axisValues): 31 | return 1 # min 32 | return 0 # neither 33 | 34 | 35 | Glyphs.clearLog() # clears log in Macro window 36 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 37 | try: 38 | font = Glyphs.font # frontmost font 39 | for selectedLayer in font.selectedLayers: 40 | if not selectedLayer.parent: 41 | continue 42 | glyph = selectedLayer.parent 43 | if glyph is None: 44 | continue 45 | 46 | glyph.beginUndo() 47 | 48 | for fontAxis in font.axes: 49 | # add font axis as smart axis if necessary: 50 | smartAxis = None # glyph.smartComponentAxes[fontAxis.name] 51 | if glyph.smartComponentAxes: 52 | for existingAxis in glyph.smartComponentAxes: 53 | if existingAxis.name == fontAxis.name: 54 | smartAxis = existingAxis 55 | if not smartAxis: 56 | smartAxis = GSSmartComponentAxis() 57 | smartAxis.name = fontAxis.name 58 | glyph.smartComponentAxes.append(smartAxis) 59 | # layer assignments: 60 | for layer in glyph.layers: 61 | layer.smartComponentPoleMapping[smartAxis.id] = minMaxForLayer(layer, fontAxis.id) 62 | 63 | glyph.endUndo() 64 | 65 | except Exception as e: 66 | Glyphs.showMacroWindow() 67 | print("\n⚠️ Error in script: Make Glyph Smart\n") 68 | import traceback 69 | print(traceback.format_exc()) 70 | print() 71 | raise e 72 | 73 | finally: 74 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 75 | -------------------------------------------------------------------------------- /Pixelfonts/Flashify Pixels.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Flashify Pixels 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds small bridges to diagonal pixel connections (where two pixel corners touch). Otherwise your counters may be lost in the Flash text engine (hence the name of the script). 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSPath, GSNode, GSLINE 9 | 10 | 11 | def karo(x, y): 12 | koordinaten = ([x - 1, y], [x, y - 1], [x + 1, y], [x, y + 1]) 13 | karo = GSPath() 14 | for xy in koordinaten: 15 | newnode = GSNode() 16 | newnode.type = GSLINE 17 | newnode.position = (xy[0], xy[1]) 18 | karo.nodes.append(newnode) 19 | karo.closed = True 20 | return karo 21 | 22 | 23 | def process(thisLayer): 24 | # thisLayer.parent.beginUndo() # undo grouping causes crashes 25 | 26 | count = 0 27 | purePathsLayer = thisLayer.copyDecomposedLayer() 28 | purePathsLayer.removeOverlap() 29 | coordinatelist = [] 30 | for thisPath in purePathsLayer.paths: 31 | for thisNode in thisPath.nodes: 32 | coordinatelist.append([thisNode.x, thisNode.y]) 33 | mylength = len(coordinatelist) 34 | for cur1 in range(mylength): 35 | for cur2 in range(cur1 + 1, mylength, 1): 36 | if coordinatelist[cur1] == coordinatelist[cur2]: 37 | [my_x, my_y] = coordinatelist[cur1] 38 | myKaro = karo(my_x, my_y) 39 | if Glyphs.versionNumber >= 3: 40 | # GLYPHS 3 41 | thisLayer.shapes.append(myKaro) 42 | else: 43 | # GLYPHS 2 44 | thisLayer.paths.append(myKaro) 45 | count += 1 46 | 47 | # thisLayer.parent.endUndo() # undo grouping causes crashes 48 | return count 49 | 50 | 51 | Glyphs.clearLog() 52 | thisFont = Glyphs.font 53 | print("Flashifying %s...\n" % thisFont.familyName) 54 | 55 | thisFont.disableUpdateInterface() 56 | try: 57 | oldGridstep = thisFont.grid 58 | if oldGridstep > 1: 59 | thisFont.grid = 1 60 | layers = thisFont.selectedLayers 61 | totalCount = 0 62 | for thisLayer in layers: 63 | karoCount = process(thisLayer) 64 | totalCount += karoCount 65 | print("🔠 Added %i diamonds in %s" % (karoCount, thisLayer.parent.name)) 66 | print("\nDone. Total diamond count: %i." % totalCount) 67 | except Exception as e: 68 | Glyphs.showMacroWindow() 69 | print("\n⚠️ Script Error:\n") 70 | import traceback 71 | print(traceback.format_exc()) 72 | print() 73 | raise e 74 | 75 | finally: 76 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 77 | 78 | # thisFont.grid = oldGridstep 79 | -------------------------------------------------------------------------------- /Components/Make Component Smart.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make Components Smart in All Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Turn the selected components into smart components, based on the axes defined in the font. 5 | """ 6 | 7 | from GlyphsApp import Glyphs, GSSmartComponentAxis 8 | 9 | # clears macro window log: 10 | Glyphs.clearLog() 11 | 12 | font = Glyphs.font 13 | fontAxisValues = [m.axes for m in font.masters] 14 | selectedLayers = font.selectedLayers 15 | if len(selectedLayers) > 1: 16 | batchProcess = True 17 | else: 18 | batchProcess = False 19 | 20 | processedGlyphs = [] 21 | for selectedLayer in selectedLayers: 22 | selectedGlyph = selectedLayer.parent 23 | print(f"Processing {selectedGlyph.name}...") 24 | for compIndex, component in enumerate(selectedLayer.components): 25 | if component.selected or batchProcess: 26 | originalGlyph = font.glyphs[component.name] 27 | 28 | # Check if component is already smart 29 | if not originalGlyph.smartComponentAxes: 30 | for i in range(len(font.axes)): 31 | newAxis = GSSmartComponentAxis() 32 | newAxis.name = font.axes[i].name 33 | # newAxis.axisTag = font.axes[i].axisTag 34 | newAxis.bottomValue = min(fontAxisValues) 35 | newAxis.topValue = max(fontAxisValues) 36 | originalGlyph.smartComponentAxes.append(newAxis) 37 | for layer in originalGlyph.layers: 38 | if layer.isMasterLayer: 39 | if font.masters[layer.associatedMasterId].axes[i] == newAxis.bottomValue: 40 | layer.smartComponentPoleMapping[originalGlyph.smartComponentAxes[newAxis.name].id] = 1 41 | if font.masters[layer.associatedMasterId].axes[i] == newAxis.topValue: 42 | layer.smartComponentPoleMapping[originalGlyph.smartComponentAxes[newAxis.name].id] = 2 43 | 44 | # Reset axis values, so they can be accessed: 45 | for layer in selectedGlyph.layers: 46 | if layer.isMasterLayer: 47 | for i in range(len(font.axes)): 48 | component = layer.components[compIndex] 49 | interpolationValue = layer.associatedFontMaster().axes[i] 50 | component.smartComponentValues[originalGlyph.smartComponentAxes[i].id] = interpolationValue 51 | 52 | # Renew selection in order to show smart glyph controls 53 | if not batchProcess: 54 | for b in range(2): 55 | component.selected = bool(b) 56 | 57 | processedGlyphs.append(originalGlyph.name) 58 | 59 | if processedGlyphs: 60 | print(f'{", ".join(processedGlyphs)} are now smart glyphs.') 61 | else: 62 | print("No glyphs changed.") 63 | -------------------------------------------------------------------------------- /Glyph Names, Notes and Unicode/Color Composites in Shade of Base Glyph.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Color Composites in Shade of Base Glyph 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Color Composites in a lighter shade of the base glyph. E.g., if your A is has a red label color, then ÄÁÀĂ... will have a lighter shade of red. 6 | """ 7 | 8 | from Foundation import NSColor, NSNotFound 9 | from GlyphsApp import Glyphs 10 | 11 | 12 | prefID = "com.mekkablue.colorCompositesInShadeOfBaseGlyph" 13 | 14 | thisFont = Glyphs.font # frontmost font 15 | thisMasterID = thisFontMaster = thisFont.selectedFontMaster.id # active master ID 16 | glyphNamesWithColors = [g.name for g in thisFont.glyphs if g.color != NSNotFound and g.category != "Mark"] 17 | 18 | 19 | def registerPref(prefID, prefName, fallbackValue): 20 | prefDomain = "%s.%s" % (prefID, prefName) 21 | Glyphs.registerDefault(prefDomain, fallbackValue) 22 | if Glyphs.defaults[prefDomain] is None: 23 | Glyphs.defaults[prefDomain] = fallbackValue 24 | 25 | 26 | def getPref(prefID, prefName): 27 | prefDomain = "%s.%s" % (prefID, prefName) 28 | return Glyphs.defaults[prefDomain] 29 | 30 | 31 | def rgbaForColorObject(colorObject): 32 | r = colorObject.redComponent() 33 | g = colorObject.greenComponent() 34 | b = colorObject.blueComponent() 35 | a = colorObject.alphaComponent() 36 | return r, g, b, a 37 | 38 | 39 | # retrieve current values: 40 | registerPref(prefID, "shadeFactor", 0.5) 41 | factor = getPref(prefID, "shadeFactor") 42 | 43 | # brings macro window to front and clears its log: 44 | Glyphs.clearLog() 45 | Glyphs.showMacroWindow() 46 | print("Shading composites:\n") 47 | 48 | thisFont.disableUpdateInterface() 49 | 50 | for thisComposite in [g for g in thisFont.glyphs if g.layers[thisMasterID].components]: 51 | firstComponent = thisComposite.layers[thisMasterID].components[0] 52 | baseGlyph = firstComponent.component 53 | if baseGlyph.colorObject: 54 | r, g, b, a = rgbaForColorObject(baseGlyph.colorObject) 55 | # brightening up RGB, keeping A the same: 56 | r += (1.0 - r) * factor 57 | g += (1.0 - g) * factor 58 | b += (1.0 - b) * factor 59 | compositeColor = NSColor.colorWithRed_green_blue_alpha_(r, g, b, a) 60 | thisComposite.colorObject = compositeColor 61 | print("✅ %03ir %03ig %03ib → %s" % (100 * r, 100 * g, 100 * b, thisComposite.name)) 62 | else: 63 | print("⚠️ %s: no color set in %s" % (thisComposite.name, baseGlyph.name)) 64 | 65 | thisFont.enableUpdateInterface() 66 | -------------------------------------------------------------------------------- /Components/Propagate Corner Components to Other Masters.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Propagate Corner Components to Other Masters 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Puts Corner Components from the current layer into other master layers, at the same point indexes. Useful if Corner components do not interpolate correctly. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, CORNER, SEGMENT, CAP 9 | # from AppKit import NSNotificationCenter 10 | SUPPORTEDTYPES = (CORNER, SEGMENT, CAP) 11 | 12 | 13 | def deleteCornerComponentsOnLayer(layer): 14 | cornerComponents = [h for h in layer.hints if h.type in SUPPORTEDTYPES] 15 | if cornerComponents: 16 | for i in range(len(cornerComponents))[::-1]: 17 | h = cornerComponents[i] 18 | layer.removeHint_(h) 19 | 20 | 21 | def pathStructure(thisLayer): 22 | layerString = "" 23 | for thisPath in thisLayer.paths: 24 | layerString += "_" 25 | for thisNode in thisPath.nodes: 26 | layerString += thisNode.type[0] 27 | return layerString 28 | 29 | 30 | def process(thisLayer): 31 | thisGlyph = thisLayer.parent 32 | targetLayers = [layer for layer in thisGlyph.layers if layer != thisLayer and pathStructure(layer) == pathStructure(thisLayer)] 33 | for targetLayer in targetLayers: 34 | deleteCornerComponentsOnLayer(targetLayer) 35 | for h in [h for h in thisLayer.hints if h.type in SUPPORTEDTYPES]: 36 | # create eqivalent corner component in target layer: 37 | newCorner = h.copy() 38 | targetLayer.hints.append(newCorner) 39 | 40 | 41 | thisFont = Glyphs.font # frontmost font 42 | thisFontMaster = thisFont.selectedFontMaster # active master 43 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 44 | 45 | if thisFont and selectedLayers: 46 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 47 | try: 48 | for thisLayer in selectedLayers: 49 | thisGlyph = thisLayer.parent 50 | print("Processing", thisGlyph.name) 51 | # thisGlyph.beginUndo() # undo grouping causes crashes 52 | process(thisLayer) 53 | # thisGlyph.endUndo() # undo grouping causes crashes 54 | 55 | except Exception as e: 56 | Glyphs.showMacroWindow() 57 | print("\n⚠️ Script Error:\n") 58 | import traceback 59 | print(traceback.format_exc()) 60 | print() 61 | raise e 62 | 63 | finally: 64 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 65 | if Glyphs.versionNumber < 3 and thisFont.currentTab: 66 | thisFont.currentTab.redraw() 67 | # NSNotificationCenter.defaultCenter().postNotificationName_object_("GSUpdateInterface", thisFont.currentTab) 68 | -------------------------------------------------------------------------------- /Glyph Names, Notes and Unicode/Reorder Unicodes of Selected Glyphs.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Reorder Unicodes of Selected Glyphs 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Reorders Unicodes so that default Unicode comes first. 6 | """ 7 | 8 | from Foundation import NSArray 9 | from GlyphsApp import Glyphs 10 | 11 | 12 | thisFont = Glyphs.font # frontmost font 13 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 14 | 15 | 16 | def reorderUnicodes(thisGlyph): 17 | defaultUnicode = Glyphs.glyphInfoForName(thisGlyph.name).unicode 18 | oldUnicodes = thisGlyph.unicodes 19 | if oldUnicodes: 20 | oldUnicodes = list(oldUnicodes) 21 | if len(oldUnicodes) > 1: 22 | if defaultUnicode: 23 | orderedUnicodes = [] 24 | try: 25 | i = oldUnicodes.index(defaultUnicode) 26 | try: 27 | orderedUnicodes.append(oldUnicodes.pop(i)) # add the default as the first one 28 | orderedUnicodes.extend(oldUnicodes) # add the rest 29 | if orderedUnicodes != oldUnicodes: 30 | print("---> %s: %s" % (thisGlyph.name, ", ".join(orderedUnicodes))) 31 | unicodeSet = NSArray.alloc().initWithArray_(orderedUnicodes) 32 | thisGlyph.setUnicodesArray_(unicodeSet) 33 | except Exception as e: 34 | print(e) 35 | print() 36 | import traceback 37 | print(traceback.format_exc()) 38 | except: 39 | print("- %s: No default (%s) among unicodes (%s); left unchanged." % (thisGlyph.name, defaultUnicode, ", ".join(oldUnicodes))) 40 | else: 41 | print("- %s: No unicode defined in Glyph Info; left unchanged (%s)" % (thisGlyph.name, ", ".join(oldUnicodes) if oldUnicodes else "-")) 42 | else: 43 | print("- %s: Only one unicode set (%s); left unchanged." % (thisGlyph.name, oldUnicodes[0])) 44 | else: 45 | print("- %s: No unicodes set, nothing to reorder." % (thisGlyph.name)) 46 | 47 | 48 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 49 | try: 50 | # brings macro window to front and clears its log: 51 | Glyphs.clearLog() 52 | Glyphs.showMacroWindow() 53 | print("Reorder Unicodes of Selected Glyphs") 54 | print("Processing Unicodes of %i selected glyphs:" % len(selectedLayers)) 55 | 56 | for thisLayer in selectedLayers: 57 | thisGlyph = thisLayer.parent 58 | reorderUnicodes(thisGlyph) 59 | 60 | except Exception as e: 61 | Glyphs.showMacroWindow() 62 | print("\n⚠️ Script Error:\n") 63 | import traceback 64 | print(traceback.format_exc()) 65 | print() 66 | raise e 67 | 68 | finally: 69 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 70 | -------------------------------------------------------------------------------- /DEVREADME.md: -------------------------------------------------------------------------------- 1 | # Developer Instructions 2 | 3 | Want to contribute to the mekkablue scripts? Fantastic! Please, consider the suggestions in this style guide to keep the code consistent and legible for everyone. 4 | 5 | Thanks! 6 | — Rafał Buchner, Rainer Scheichelbauer 7 | 8 | ## Variable and constant names 9 | 10 | * Keep your names legible: use `points`, not `p`. 11 | * Do not repeat the type of the object in the variable name: `layers`, not `layerList` or `listOfLayers`. There are still remainders of ancient code where I was guilty of doing this myself, but we are cleaning this up, step by step ourselves. 12 | * Please use `camelCase` , not `under_score_separations`. We know it is not very pythonic, but we have a lot of PyObjC pieces in our code, where underscores have a special meaning. 13 | 14 | ## Code conventions 15 | 16 | ** Note about yapf** It has some bugs that messes with the indentain of closing parentesis/braces! 17 | * Use the `.style.yapf` that is provided on the root level of the repo: `yapf --style .style.yapf -i path/to/script.py` 18 | * Use tabs, not spaces, for indentation. 19 | * Be perfomance aware. 20 | * Wherever possible, use tuples instead of lists. 21 | * Consider `myTuple = (n for n in myList)` instead of `tuple(myList)`. 22 | * When in doubt, use the *timer* snippet (see below about snippets). 23 | 24 | ### To run `flake8` 25 | 26 | * Use this command in Terminal the root of the repo: `flake8` 27 | 28 | ### To run `mypy` 29 | 30 | * tell mypy where to find the GlyphsApp module: run this in the Terminal: `export MYPYPATH="~/Code/Glyphs/Glyphs/Scripts/:$MYPYPATH"` (ajust to full paths) 31 | * run mypy: `mypy --ignore-missing-imports .` 32 | 33 | ## Reporting 34 | 35 | * Report into Macro Window with `print()` functions. 36 | * Use ‘Report for ...’ titles, and consider clearing the Macro Window log at the beginning with `Glyphs.clearLog()` 37 | * Use emojis (e.g. ⚠️✅❌☑️💾↔️🔠) and indentations to keep it legible and allow orientation for the user. 38 | * Do not open the Macro Window unless the report is the whole purpose of the script. Instead, consider a floating notification or dialog to tell the user that the script completed and that details are available in the Macro Window, e.g., like this: `Glyphs.showNotification("Font X: done", "Brief statistic of what happened. Details in Macro Window.")`. If you use a dialog, opening the Macro Window can be a button. 39 | 40 | 41 | ## Snippets 42 | 43 | Are you using Sublime Text or TextMate? For frequent code pieces, consider the [Python for Glyphs snippets](https://github.com/mekkablue/Python-for-Glyphs "Python code snippets for the Glyphs.app font editor, for Sublime Text and TextMate") on GitHub. 44 | -------------------------------------------------------------------------------- /Post Production/fixgdef.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | python3 fixgdef.py -h ... help 4 | python3 fixgdef.py *.ttf ... apply to all TTFs in current dir 5 | """ 6 | 7 | from fontTools import ttLib 8 | from argparse import ArgumentParser 9 | 10 | 11 | parser = ArgumentParser(description="Fix GDEF definition of spacing, non-combining marks. Will switch to class 1 (‘base glyph’, single character, spacing glyph) if necessary.") 12 | 13 | parser.add_argument( 14 | "fonts", 15 | nargs="+", 16 | metavar="font", 17 | help="One or more OTF/TTF files", 18 | ) 19 | 20 | 21 | def fixGDEFinFont(font): 22 | """Takes a ttLib.TTFont as argument.""" 23 | madeChanges = False 24 | 25 | if "GDEF" not in font.keys(): 26 | print("⚠️ No GDEF table found, skipping file.\n") 27 | return madeChanges 28 | 29 | gdef = font["GDEF"].table 30 | if not hasattr(gdef, "MarkGlyphSetsDef") or not gdef.MarkGlyphSetsDef: 31 | print("⚠️ No MarkGlyphSetsDef found in GDEF table.") 32 | else: 33 | print("Scanning MarkGlyphSetsDef...") 34 | legacyMarks = ( 35 | "dieresis", 36 | "dotaccent", 37 | "grave", 38 | "acute", 39 | "hungarumlaut", 40 | "circumflex", 41 | "caron", 42 | "breve", 43 | "ring", 44 | "tilde", 45 | "macron", 46 | "cedilla", 47 | "ogonek", 48 | "uni02BB" 49 | ) 50 | for coverage in gdef.MarkGlyphSetsDef.Coverage: 51 | for i in range(len(coverage.glyphs) - 1, -1, -1): 52 | glyph = coverage.glyphs[i] 53 | if glyph in legacyMarks: 54 | coverage.glyphs.pop(i) 55 | print(f"\t🚫 Removed {glyph} from GDEF.MarkGlyphSetsDef") 56 | madeChanges = True 57 | elif not (glyph.startswith("uni03") or "comb" in glyph): 58 | print(f"\t❓ {glyph}") 59 | # else: 60 | # print(f"\t✅ {glyph}") 61 | 62 | if not gdef.GlyphClassDef: 63 | print("⚠️ No GlyphClassDef found in GDEF table.") 64 | else: 65 | print("Scanning GlyphClassDef...") 66 | for legacyMark in legacyMarks: 67 | if legacyMark in gdef.GlyphClassDef.classDefs: 68 | classType = gdef.GlyphClassDef.classDefs[legacyMark] 69 | if classType != 1: 70 | gdef.GlyphClassDef.classDefs[legacyMark] = 1 71 | print(f"\t👨🏻‍🔧 Switched {legacyMark} from class {classType} to 1") 72 | madeChanges = True 73 | 74 | return madeChanges 75 | 76 | 77 | arguments = parser.parse_args() 78 | fonts = arguments.fonts 79 | for fontpath in fonts: 80 | print(f"\n📄 {fontpath}") 81 | font = ttLib.TTFont(fontpath) 82 | changesMade = fixGDEFinFont(font) 83 | if changesMade: 84 | font.save(fontpath, reorderTables=False) 85 | print(f"💾 Saved {fontpath}\n") 86 | else: 87 | print("🤷🏻‍♀️ No changes made. File left unchanged.") 88 | 89 | print("✅ Done.") 90 | -------------------------------------------------------------------------------- /Anchors/Find and Replace in Anchor Names.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Find And Replace In Anchor Names 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Replaces strings in anchor names of all selected glyphs. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | 13 | class SearchAndReplaceInAnchorNames(mekkaObject): 14 | prefDict = { 15 | "searchFor": "", 16 | "replaceBy": "", 17 | } 18 | 19 | def __init__(self): 20 | windowWidth = 511 21 | windowHeight = 52 22 | self.w = vanilla.FloatingWindow( 23 | (windowWidth, windowHeight), # default window size 24 | "Search And Replace In Anchor Names", # window title 25 | autosaveName=self.domain("mainwindow") # stores last window position and size 26 | ) 27 | 28 | # UI elements: 29 | baseline = 14 30 | self.w.textSearch = vanilla.TextBox((20, baseline + 3, 67, 16), "Search") 31 | self.w.searchFor = vanilla.EditText((20 + 50, baseline, 135, 22), "tip") 32 | 33 | self.w.textReplace = vanilla.TextBox((212, baseline + 3, 67, 16), "Replace") 34 | self.w.replaceBy = vanilla.EditText((212 + 57, baseline, 135, 22), "top") 35 | 36 | self.w.replaceButton = vanilla.Button((-95, baseline + 1, -20, 19), "Replace", callback=self.SearchAndReplaceInAnchorNamesMain) 37 | self.w.setDefaultButton(self.w.replaceButton) 38 | 39 | # Load Settings: 40 | self.LoadPreferences() 41 | 42 | # Open window and focus on it: 43 | self.w.open() 44 | self.w.makeKey() 45 | 46 | def SearchAndReplaceInAnchorNamesMain(self, sender): 47 | searchString = self.w.searchFor.get() 48 | replaceString = self.w.replaceBy.get() 49 | 50 | thisFont = Glyphs.font # frontmost font 51 | listOfSelectedLayers = thisFont.selectedLayers # active layers of currently selected glyphs 52 | 53 | for thisLayer in listOfSelectedLayers: # loop through layers 54 | thisGlyph = thisLayer.parent 55 | reportString = "Anchors renamed in %s:" % thisGlyph.name 56 | displayReportString = False 57 | 58 | for thisGlyphLayer in thisGlyph.layers: 59 | for thisAnchor in thisGlyphLayer.anchors: 60 | oldAnchorName = thisAnchor.name 61 | newAnchorName = oldAnchorName.replace(searchString, replaceString) 62 | if oldAnchorName != newAnchorName: 63 | thisAnchor.name = newAnchorName 64 | reportString += "\n layer '%s': %s > %s" % (thisGlyphLayer.name, oldAnchorName, newAnchorName) 65 | displayReportString = True 66 | 67 | if displayReportString: 68 | print(reportString) 69 | 70 | self.SavePreferences() 71 | 72 | self.w.close() # delete if you want window to stay open 73 | 74 | 75 | SearchAndReplaceInAnchorNames() 76 | -------------------------------------------------------------------------------- /Images/Toggle Image Lock.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Toggle Image Lock 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Floating Window for toggling the locked status of selected glyphs. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | 11 | 12 | class ToggleImageLock(object): 13 | 14 | def __init__(self): 15 | # Window 'self.w': 16 | windowWidth = 250 17 | windowHeight = 60 18 | windowWidthResize = 500 # user can resize width by this value 19 | windowHeightResize = 0 # user can resize height by this value 20 | self.w = vanilla.FloatingWindow( 21 | (windowWidth, windowHeight), # default window size 22 | "Toggle Image Lock", # window title 23 | minSize=(windowWidth, windowHeight), # minimum size (for resizing) 24 | maxSize=(windowWidth + windowWidthResize, windowHeight + windowHeightResize), # maximum size (for resizing) 25 | autosaveName=self.domain("mainwindow") # stores last window position and size 26 | ) 27 | 28 | currentWidth = self.w.getPosSize()[2] 29 | # A tuple of form *(left, top, width, height)* representing the window's 30 | # position and size. 31 | 32 | self.w.lockButton = vanilla.Button((15, 10, currentWidth / 2 - 10, -10), u"🔒 Lock", callback=self.ToggleImageLockMain) 33 | self.w.unlockButton = vanilla.Button((currentWidth / 2 + 10, 10, -15, -10), u"🔓 Unlock", callback=self.ToggleImageLockMain) 34 | self.w.setDefaultButton(self.w.unlockButton) 35 | self.w.bind("resize", self.resizeButtons) 36 | 37 | # Open window and focus on it: 38 | self.w.open() 39 | self.w.makeKey() 40 | 41 | def resizeButtons(self, sender): 42 | currentWidth = self.w.getPosSize()[2] 43 | print(currentWidth) 44 | self.w.lockButton.setPosSize((15, 10, currentWidth / 2 - 10, -10)) 45 | self.w.unlockButton.setPosSize((currentWidth / 2 + 10, 10, -15, -10)) 46 | 47 | def ToggleImageLockMain(self, sender): 48 | try: 49 | lockedStatus = True 50 | if sender == self.w.unlockButton: 51 | lockedStatus = False 52 | 53 | thisFont = Glyphs.font # frontmost font 54 | listOfSelectedLayers = thisFont.selectedLayers # active layers of currently selected glyphs 55 | for thisLayer in listOfSelectedLayers: # loop through layers 56 | if thisLayer.backgroundImage: 57 | thisLayer.backgroundImage.locked = lockedStatus 58 | if thisLayer.parent: 59 | status = "Locked" if thisLayer.backgroundImage.locked else "Unlocked" 60 | print("%s image in %s." % (status, thisLayer.parent.name)) 61 | except Exception as e: 62 | # brings macro window to front and reports error: 63 | Glyphs.showMacroWindow() 64 | print("Toggle Image Lock Error: %s" % e) 65 | 66 | 67 | ToggleImageLock() 68 | -------------------------------------------------------------------------------- /Spacing/Change Metrics by Percentage.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Change Metrics by Percentage 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Increase sidebearings of selected glyphs by a percentage value. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | 13 | class ChangeMetricsbyPercentage(mekkaObject): 14 | prefDict = { 15 | "LSB": True, 16 | "RSB": True, 17 | "changeValue": "+10.0", 18 | } 19 | 20 | def __init__(self): 21 | windowWidth = 390 22 | windowHeight = 42 23 | self.w = vanilla.FloatingWindow( 24 | (windowWidth, windowHeight), 25 | "Change Metrics of Selected Glyphs by Percentage", 26 | autosaveName=self.domain("mainwindow") 27 | ) 28 | 29 | linePos, inset = 12, 15 30 | xPos = inset 31 | self.w.text_1 = vanilla.TextBox((xPos, linePos + 2, 50, 14), "Increase", sizeStyle='small') 32 | xPos += 55 33 | self.w.LSB = vanilla.CheckBox((xPos, linePos, 40, 18), "LSB", value=True, sizeStyle='small', callback=self.SavePreferences) 34 | xPos += 44 35 | self.w.RSB = vanilla.CheckBox((xPos, linePos, 50, 18), "RSB by", value=True, sizeStyle='small', callback=self.SavePreferences) 36 | xPos += 57 37 | self.w.changeValue = vanilla.EditText((xPos, linePos - 1, 55, 20), "+10.0", sizeStyle='small') 38 | xPos += 56 39 | self.w.text_3 = vanilla.TextBox((xPos, linePos + 2, 17, 14), "%", sizeStyle='small') 40 | xPos += 20 41 | self.w.revertButton = vanilla.Button((xPos, linePos, 60, 19), "⟲ Revert", sizeStyle='small', callback=self.ChangeMetricsbyPercentageMain) 42 | xPos += 66 43 | self.w.runButton = vanilla.Button((xPos, linePos, 60, 19), "Change", sizeStyle='small', callback=self.ChangeMetricsbyPercentageMain) 44 | 45 | self.w.setDefaultButton(self.w.runButton) 46 | 47 | self.LoadPreferences() 48 | 49 | self.w.open() 50 | self.w.makeKey() 51 | 52 | def ChangeMetricsbyPercentageMain(self, sender): 53 | try: 54 | Font = Glyphs.font 55 | selectedLayers = Font.selectedLayers 56 | 57 | changeLSB = self.w.LSB.get() 58 | changeRSB = self.w.RSB.get() 59 | change = (100.0 + float(self.w.changeValue.get())) / 100.0 60 | 61 | if sender == self.w.revertButton: 62 | change = 1.0 / change 63 | 64 | for thisLayer in selectedLayers: 65 | if len(thisLayer.paths) > 0 or len(thisLayer.components) > 0: 66 | if changeLSB: 67 | thisLayer.LSB *= change 68 | if changeRSB: 69 | thisLayer.RSB *= change 70 | 71 | self.SavePreferences() 72 | 73 | # self.w.close() 74 | except Exception as e: 75 | # brings macro window to front and reports error: 76 | Glyphs.showMacroWindow() 77 | print(" Error: %s" % e) 78 | 79 | 80 | ChangeMetricsbyPercentage() 81 | -------------------------------------------------------------------------------- /Color Fonts/Convert Master Colors to CPAL Palette.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Convert Master Colors to CPAL Palette 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Will look for ‘Master Color’ parameters in the font masters and then create a ‘Color Palettes’ parameter in Font Info > Font with the same color. Will default to black (for missing Master Color parameters). Will add Dark Mode master colors as second palette. 6 | """ 7 | 8 | from AppKit import NSColor 9 | from Foundation import NSHeight, NSMutableArray 10 | from GlyphsApp import Glyphs 11 | 12 | 13 | def colorForMaster(thisMaster, parameterName="Master Color"): 14 | color = thisMaster.customParameters[parameterName] 15 | if not color: 16 | color = NSColor.blackColor() 17 | color = color.colorUsingColorSpaceName_("NSCalibratedRGBColorSpace") 18 | print(f"\t{parameterName}: {color.redComponent():.3f}r {color.greenComponent():.3f}g {color.blueComponent():.3f}b {color.alphaComponent():.3f}a") 19 | return color 20 | 21 | 22 | Glyphs.clearLog() # clears log in Macro window 23 | print("Status Report: Convert Master Colors to CPAL Palette") 24 | if Glyphs.versionNumber < 4: 25 | try: 26 | splitview = Glyphs.delegate().macroPanelController().consoleSplitView() 27 | splitview.setPosition_ofDividerAtIndex_(NSHeight(splitview.frame()) * 0.2, 0) 28 | except Exception as e: 29 | print(f"\nFailed resetting the macro panel divider: {e}") 30 | import traceback 31 | print(traceback.format_exc()) 32 | 33 | thisFont = Glyphs.font # frontmost font 34 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 35 | try: 36 | paletteColors = [] 37 | paletteColorsDark = [] 38 | for thisMaster in thisFont.masters: 39 | print(f"Analyzing master ‘{thisMaster.name}’...") 40 | paletteColors.append(colorForMaster(thisMaster)) 41 | paletteColorsDark.append(colorForMaster(thisMaster, parameterName="Master Color Dark")) 42 | cpalPalettes = NSMutableArray.arrayWithObjects_(paletteColors, paletteColorsDark) 43 | if cpalPalettes: 44 | print("🌈 Adding Color Palettes parameter in Font Info > Font...") 45 | thisFont.customParameters["Color Palettes"] = cpalPalettes 46 | print("✅ Done.") 47 | # open Font Info: 48 | for doc in Glyphs.documents: 49 | if doc.font == thisFont: 50 | doc.windowController().showFontInfoWindowWithTabSelected_(0) 51 | break 52 | else: 53 | print("🚫 No palette added.") 54 | Glyphs.showMacroWindow() 55 | 56 | except Exception as e: 57 | Glyphs.showMacroWindow() 58 | print("\n⚠️ Error in script: Convert Master Colors to CPAL Palette\n") 59 | import traceback 60 | print(traceback.format_exc()) 61 | print() 62 | raise e 63 | finally: 64 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 65 | -------------------------------------------------------------------------------- /Hinting/Add Hints for Selected Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Add Hints to Selected Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Adds hints for the selected nodes. Tries to guess whether it should be H or V. If exactly one node inside a zone is selected, it will add a Ghost Hint. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSHint, GSNode 9 | 10 | 11 | Font = Glyphs.font 12 | FontMaster = Font.selectedFontMaster 13 | thisLayer = Font.selectedLayers[0] 14 | try: 15 | # until v2.1: 16 | selection = thisLayer.selection() 17 | except: 18 | # since v2.2: 19 | selection = thisLayer.selection 20 | 21 | thisSelection = [n for n in selection if isinstance(n, GSNode)] 22 | numberOfSelectedNodes = len(thisSelection) 23 | 24 | 25 | def hintTypeForY(yValue): 26 | for thisZone in FontMaster.alignmentZones: 27 | try: 28 | # app versions 1.4.4 and later 29 | zonePosition = float(thisZone.position) 30 | zoneSize = float(thisZone.size) 31 | except: 32 | # app versions before 1.4.4 33 | zonePosition = thisZone.position() 34 | zoneSize = thisZone.size() 35 | if yValue == sorted([zonePosition, zonePosition + zoneSize, yValue])[1]: 36 | if zoneSize > 0: 37 | return -1 38 | elif zoneSize < 0: 39 | return 1 40 | return False 41 | 42 | 43 | if numberOfSelectedNodes == 1: 44 | # Ghost Hint 45 | thisNode = thisSelection[0] 46 | thisNodePosition = thisNode.y 47 | hintType = hintTypeForY(thisNodePosition) 48 | if hintType is not False: 49 | newHint = GSHint() 50 | newHint.originNode = thisNode 51 | newHint.type = hintType 52 | newHint.horizontal = True 53 | thisLayer.hints.append(newHint) 54 | 55 | elif numberOfSelectedNodes % 2 == 0: 56 | # Determine horizontal/vertical hints: 57 | xCoordinates = sorted([n.x for n in thisSelection]) 58 | yCoordinates = sorted([n.y for n in thisSelection]) 59 | xDiff = xCoordinates[-1] - xCoordinates[0] 60 | yDiff = yCoordinates[-1] - yCoordinates[0] 61 | isHorizontal = yDiff > xDiff 62 | if isHorizontal: 63 | sortedListOfNodes = sorted(thisSelection, key=lambda n: n.y) 64 | else: 65 | sortedListOfNodes = sorted(thisSelection, key=lambda n: n.x) 66 | 67 | # Add Hints: 68 | for i in range(numberOfSelectedNodes // 2): 69 | firstIndex = (i + 1) * 2 - 2 70 | secondIndex = (i + 1) * 2 - 1 71 | firstNode = sortedListOfNodes[firstIndex] 72 | secondNode = sortedListOfNodes[secondIndex] 73 | 74 | newHint = GSHint() 75 | newHint.originNode = firstNode 76 | newHint.targetNode = secondNode 77 | newHint.type = 0 78 | newHint.horizontal = isHorizontal 79 | thisLayer.addHint_(newHint) 80 | else: 81 | Glyphs.clearLog() 82 | Glyphs.showMacroWindow() 83 | print("Error: Either 1 node, or an even number of nodes must be selected.") 84 | -------------------------------------------------------------------------------- /Color Fonts/Randomly Distribute Shapes on Color Layers.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Randomly Distribute Shapes on Color Layers 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Take the shapes of the fallback master layer, and randomly copy them onto the available CPAL/COLR color layers. ⚠️ Will overwrite contents of existing color layers unless you hold down Cmd+Shift. 6 | """ 7 | 8 | import random 9 | from AppKit import NSEvent 10 | from copy import copy 11 | from GlyphsApp import Glyphs, GSGlyph 12 | 13 | random.seed() 14 | 15 | 16 | def process(thisGlyph, shouldPreserveExistingShapes=False): 17 | for thisMaster in thisGlyph.parent.masters: 18 | mID = thisMaster.id 19 | masterLayer = thisGlyph.layers[mID] 20 | availableColorLayers = [layer for layer in thisGlyph.layers if layer.associatedMasterId == mID and layer.attributes and "colorPalette" in layer.attributes.keys()] 21 | if availableColorLayers: 22 | if not shouldPreserveExistingShapes: 23 | for colorLayer in availableColorLayers: 24 | colorLayer.clear() 25 | for thisShape in masterLayer.shapes: 26 | targetLayer = random.choice(availableColorLayers) 27 | targetLayer.shapes.append(copy(thisShape)) 28 | 29 | 30 | Glyphs.clearLog() # clears log in Macro window 31 | print("Randomly Distribute Shapes on Color Layers") 32 | 33 | keysPressed = NSEvent.modifierFlags() 34 | commandKey = 1048576 35 | shiftKey = 131072 36 | commandKeyPressed = keysPressed & commandKey == commandKey 37 | shiftKeyPressed = keysPressed & shiftKey == shiftKey 38 | shouldPreserveExistingShapes = commandKeyPressed and shiftKeyPressed 39 | 40 | thisFont = Glyphs.font # frontmost font 41 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 42 | selectedGlyphs: list[GSGlyph] = [] 43 | if selectedLayers: 44 | selectedGlyphs = [layer.parent for layer in selectedLayers] 45 | print(f"{len(selectedGlyphs)} selected glyphs in font ‘{thisFont.familyName}’...") 46 | else: 47 | selectedGlyphs = thisFont.glyphs 48 | print(f"No glyph selected, processing all {len(selectedGlyphs)} glyphs in font ‘{thisFont.familyName}’...") 49 | 50 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 51 | try: 52 | for thisGlyph in selectedGlyphs: 53 | print(f"Distributing shapes on {thisGlyph.name}") 54 | thisGlyph.beginUndo() # begin undo grouping 55 | process(thisGlyph, shouldPreserveExistingShapes) 56 | thisGlyph.endUndo() # end undo grouping 57 | print("✅ Done.") 58 | except Exception as e: 59 | Glyphs.showMacroWindow() 60 | print("\n⚠️ Error in script: Randomly Distribute Shapes on Color Layers\n") 61 | import traceback 62 | print(traceback.format_exc()) 63 | print() 64 | raise e 65 | finally: 66 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 67 | -------------------------------------------------------------------------------- /App/Set Tool Shortcuts.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Set Tool Shortcuts 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Set Shortcuts for tools in toolbar. 6 | """ 7 | 8 | import vanilla 9 | from GlyphsApp import Glyphs 10 | from mekkablue import mekkaObject 11 | 12 | shortcuts = { 13 | "AnnotationTool": "a", 14 | "DrawTool": "p", 15 | "HandTool": "h", 16 | "MeasurementTool": "l", 17 | "OtherPathsTool": "e", 18 | "PenTool": "b", 19 | "PrimitivesTool": "f", 20 | "SelectTool": "v", 21 | "SelectAllLayersTool": "v", 22 | "TextTool": "t", 23 | "RotateTool": "r", 24 | "ScaleTool": "s", 25 | "TrueTypeTool": "i", 26 | "ZoomTool": "z", 27 | } 28 | 29 | 30 | class SetToolShortcuts(mekkaObject): 31 | 32 | def __init__(self): 33 | position = 14 34 | lineheight = 23 35 | 36 | # Window 'self.w': 37 | windowWidth = 200 38 | windowHeight = lineheight * len(shortcuts) + 20 39 | self.w = vanilla.FloatingWindow( 40 | (windowWidth, windowHeight), # default window size 41 | "Set Tool Shortcuts", # window title 42 | autosaveName=self.domain("mainwindow") # stores last window position and size 43 | ) 44 | 45 | # UI elements: 46 | for tool in sorted(shortcuts.keys()): 47 | shortcut = Glyphs.defaults["%s.Hotkey" % tool] 48 | if not shortcut: 49 | shortcut = shortcuts[tool] 50 | setattr(self.w, "text_%s" % tool, vanilla.TextBox((15, position + 2, 115, 14), tool, sizeStyle='small')) 51 | shortcut = shortcut.upper() if shortcut != "ß" else shortcut # do not capitalize ß because SF font is buggy 52 | setattr(self.w, "edit_%s" % tool, vanilla.EditText((15 + 115 + 15, position - 1, -15, 20), shortcut, sizeStyle='small', callback=self.changeShortcut)) 53 | exec("self.w.edit_%s.setPlaceholder('%s')" % (tool, tool)) 54 | position += lineheight 55 | 56 | # Open window and focus on it: 57 | self.w.open() 58 | self.w.makeKey() 59 | 60 | 61 | def changeShortcut(self, sender): 62 | try: 63 | tool = sender.getPlaceholder() 64 | newShortcut = sender.get() 65 | print("%s: '%s'" % (tool, sender.get())) 66 | if len(newShortcut) > 0: 67 | if newShortcut: 68 | sender.set(newShortcut[-1].upper()) 69 | newShortcut = newShortcut[-1].lower() 70 | Glyphs.defaults["%s.Hotkey" % tool] = newShortcut 71 | print("New Shortcut for %s: %s" % (tool, newShortcut)) 72 | sender.selectAll() 73 | else: 74 | print("Resetting Shortcut for %s: %s" % (tool, newShortcut)) 75 | del Glyphs.defaults["%s.Hotkey" % tool] 76 | sender.set(shortcuts[tool].upper()) 77 | 78 | except Exception as e: 79 | # brings macro window to front and reports error: 80 | Glyphs.showMacroWindow() 81 | print("Set Tool Shortcuts Error: %s" % e) 82 | import traceback 83 | print(traceback.format_exc()) 84 | 85 | 86 | SetToolShortcuts() 87 | -------------------------------------------------------------------------------- /Guides/Guides through All Selected Nodes.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Guides through All Selected Nodes 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates guides through all selected nodes. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSGuide, GSGuideLine, GSNode, GSAnchor, addPoints 9 | from mekkablue.geometry import angle 10 | 11 | thisFont = Glyphs.font # frontmost font 12 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 13 | 14 | 15 | def newGuide(position, angle=0): 16 | try: 17 | # GLYPHS 3 18 | newGuide = GSGuide() 19 | except: 20 | # GLYPHS 2 21 | newGuide = GSGuideLine() 22 | newGuide.position = position 23 | newGuide.angle = angle 24 | return newGuide 25 | 26 | 27 | def isThereAlreadyAGuideWithTheseProperties(thisLayer, guideposition, guideangle): 28 | if guideangle < 0: 29 | guideangle += 180 30 | if guideangle > 180: 31 | guideangle -= 180 32 | for thisGuide in thisLayer.guides: 33 | thisAngle = thisGuide.angle 34 | if thisAngle < 0: 35 | thisAngle += 180 36 | if thisAngle > 180: 37 | thisAngle -= 180 38 | if abs(thisAngle - guideangle) < 0.01 and abs(thisGuide.position.x - guideposition.x) < 0.01 and abs(thisGuide.position.y - guideposition.y) < 0.01: 39 | return True 40 | return False 41 | 42 | 43 | if len(selectedLayers) == 1: 44 | thisLayer = selectedLayers[0] 45 | thisGlyph = thisLayer.parent 46 | currentPointSelection = [point.position for point in thisLayer.selection if isinstance(point, (GSNode, GSAnchor))] 47 | 48 | # thisGlyph.beginUndo() # undo grouping causes crashes 49 | try: 50 | if len(currentPointSelection) > 1: 51 | # clear selection: 52 | thisLayer.clearSelection() 53 | currentPointSelection.append(currentPointSelection[0]) 54 | for i, j in enumerate(range(1, len(currentPointSelection))): 55 | point1 = currentPointSelection[i] 56 | point2 = currentPointSelection[j] 57 | angleBetweenPoints = angle(point1, point2) 58 | middlePoint = addPoints(point1, point2) 59 | middlePoint.x *= 0.5 60 | middlePoint.y *= 0.5 61 | 62 | # create guide and add it to layer: 63 | if not isThereAlreadyAGuideWithTheseProperties(thisLayer, middlePoint, angleBetweenPoints): 64 | guideBetweenPoints = newGuide(middlePoint, angleBetweenPoints) 65 | thisLayer.guides.append(guideBetweenPoints) 66 | 67 | # select it: 68 | thisLayer.selection.append(guideBetweenPoints) 69 | 70 | elif len(currentPointSelection) == 1: 71 | point = currentPointSelection[0] 72 | guide = newGuide(point) 73 | thisLayer.guides.append(guide) 74 | 75 | # select only guide: 76 | thisLayer.clearSelection() 77 | thisLayer.selection.append(guide) 78 | 79 | except Exception as e: 80 | raise e 81 | # finally: 82 | # thisGlyph.endUndo() # undo grouping causes crashes 83 | -------------------------------------------------------------------------------- /Glyph Names, Notes and Unicode/Convert to Uppercase.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Convert to Uppercase 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Turns lowercase names into uppercase names, e.g., `a` → `A`, `ccaron` → `Ccaron`, `aeacute` → `AEacute`, etc. 6 | """ 7 | 8 | from GlyphsApp import Glyphs 9 | 10 | 11 | def uppercaseGlyphName(thisGlyph): 12 | originalGlyphName = thisGlyph.name 13 | glyphNameParts = originalGlyphName.split(".") 14 | coreName = glyphNameParts[0] 15 | coreInfo = Glyphs.glyphInfoForName(coreName) 16 | lowercaseCharacter = coreInfo.unicharString() 17 | if lowercaseCharacter is None: 18 | print("⚠️ Cannot determine character for: %s" % originalGlyphName) 19 | return 0 20 | else: 21 | uppercaseCharacter = lowercaseCharacter.upper() 22 | if lowercaseCharacter == uppercaseCharacter: 23 | print("🆗 %s: unchanged, cannot be uppercased." % originalGlyphName) 24 | return 0 25 | else: 26 | uppercaseCoreName = Glyphs.niceGlyphName(uppercaseCharacter) 27 | glyphNameParts[0] = uppercaseCoreName 28 | uppercaseGlyphName = ".".join(glyphNameParts) 29 | thisFont = thisGlyph.parent 30 | if thisFont.glyphs[uppercaseGlyphName]: 31 | print("❌ %s: cannot convert to %s, glyph already exists." % (originalGlyphName, uppercaseGlyphName)) 32 | return 0 33 | else: 34 | thisGlyph.name = uppercaseGlyphName 35 | thisGlyph.updateGlyphInfo() 36 | print( 37 | "✅ %s → %s" % ( 38 | originalGlyphName, 39 | thisGlyph.name if thisGlyph.name == uppercaseGlyphName else "%s → %s (updated glyph info)" % (uppercaseGlyphName, thisGlyph.name), 40 | ) 41 | ) 42 | return 1 43 | 44 | 45 | Glyphs.clearLog() # clears macro window log 46 | Font = Glyphs.font 47 | selectedGlyphs = [layer.parent for layer in Font.selectedLayers] 48 | countSelectedGlyphs = len(selectedGlyphs) 49 | convertedCount = 0 50 | print("Converting %i selected glyphs to uppercase:\n" % countSelectedGlyphs) 51 | 52 | Font.disableUpdateInterface() 53 | try: 54 | for thisGlyph in selectedGlyphs: 55 | convertedCount += uppercaseGlyphName(thisGlyph) 56 | except Exception as e: 57 | Glyphs.showMacroWindow() 58 | print("\n⚠️ Script Error:\n") 59 | import traceback 60 | print(traceback.format_exc()) 61 | print() 62 | raise e 63 | finally: 64 | Font.enableUpdateInterface() 65 | 66 | # Floating notification: 67 | Glyphs.showNotification( 68 | "%s: UC Conversion Finished" % (Font.familyName), 69 | "Of %i selected glyph%s, %i %s converted to uppercase. Details in Macro Window." % ( 70 | countSelectedGlyphs, 71 | "" if countSelectedGlyphs == 1 else "s", 72 | convertedCount, 73 | "was" if convertedCount == 1 else "were", 74 | ), 75 | ) 76 | 77 | print("\n%i glyph%s converted to uppercase.\nDone." % ( 78 | convertedCount, 79 | "" if convertedCount == 1 else "s", 80 | )) 81 | -------------------------------------------------------------------------------- /Build Glyphs/Build Ldot and ldot.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Build Ldot and ldot 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Builds Ldot, ldot and ldot.sc from existing L and periodcentered.loclCAT(.case/.sc). 6 | """ 7 | 8 | from Foundation import NSPoint 9 | from GlyphsApp import Glyphs, GSGlyph, GSComponent 10 | 11 | 12 | def buildLdot(targetGlyphName, baseName, accentName): 13 | try: 14 | print("\n%s + %s = %s" % (baseName, accentName, targetGlyphName)) 15 | for thisMaster in thisFont.masters: 16 | print("\tProcessing master: %s" % thisMaster.name) 17 | thisMasterID = thisMaster.id 18 | offsetAccent = thisFont.glyphs[baseName].layers[thisMasterID].width 19 | accentWidth = thisFont.glyphs[accentName].layers[thisMasterID].width 20 | baseWidth = thisFont.glyphs[baseName].layers[thisMasterID].width 21 | targetGlyph = thisFont.glyphs[targetGlyphName] 22 | if not targetGlyph: 23 | targetGlyph = GSGlyph(targetGlyphName) 24 | thisFont.glyphs.append(targetGlyph) 25 | else: 26 | targetGlyph.leftMetricsKey = None 27 | targetGlyph.rightMetricsKey = None 28 | targetLayer = targetGlyph.layers[thisMasterID] 29 | try: 30 | targetLayer.shapes = None 31 | targetLayer.shapes.append(GSComponent(baseName)) 32 | targetLayer.shapes.append(GSComponent(accentName, NSPoint(offsetAccent, 0.0))) 33 | except: 34 | targetLayer.components = [] 35 | targetLayer.components.append(GSComponent(baseName)) 36 | targetLayer.components.append(GSComponent(accentName, NSPoint(offsetAccent, 0.0))) 37 | for thisComp in targetLayer.components: 38 | thisComp.disableAlignment = False 39 | targetLayer.width = baseWidth + accentWidth 40 | targetLayer.leftMetricsKey = None 41 | targetLayer.rightMetricsKey = None 42 | return 1 43 | except: 44 | return 0 45 | 46 | 47 | thisFont = Glyphs.font # frontmost font 48 | Glyphs.clearLog() # clears macro window log 49 | 50 | print(f"Report for {thisFont.familyName}:") 51 | if thisFont.filepath: 52 | print(thisFont.filepath) 53 | 54 | buildGlyphs = [ 55 | ("ldot", "l", "periodcentered.loclCAT"), 56 | ("Ldot", "L", "periodcentered.loclCAT.case"), 57 | ("ldot.sc", "l.sc", "periodcentered.loclCAT.sc"), 58 | ] 59 | 60 | createdGlyphs = [] 61 | tabText = "" 62 | for glyphInfo in buildGlyphs: 63 | target = glyphInfo[0] 64 | base = glyphInfo[1] 65 | accent = glyphInfo[2] 66 | 67 | if thisFont.glyphs[base] and thisFont.glyphs[accent]: 68 | if buildLdot(target, base, accent): 69 | createdGlyphs.append(target) 70 | tabText += f"/{target}/{base}" 71 | 72 | reportMessage = "%i glyph%s created" % ( 73 | len(createdGlyphs), 74 | "" if len(createdGlyphs) == 1 else "s", 75 | ) 76 | 77 | print(f"\nDone: {reportMessage}.") 78 | 79 | if tabText: 80 | thisFont.newTab(tabText) 81 | else: 82 | Glyphs.showMacroWindow() 83 | -------------------------------------------------------------------------------- /Glyph Names, Notes and Unicode/Convert to Lowercase.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Convert to Lowercase 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Turns uppercase names into lowercase names, e.g., `A` → `a`, `Ccaron` → `ccaron`, `AEacute` → `aeacute`, etc. Useful for smallcap glyphs. 6 | """ 7 | from GlyphsApp import Glyphs 8 | 9 | 10 | def lowercaseGlyphName(thisGlyph): 11 | originalGlyphName = thisGlyph.name 12 | glyphNameParts = originalGlyphName.split(".") 13 | coreName = glyphNameParts[0] 14 | coreInfo = Glyphs.glyphInfoForName(coreName) 15 | uppercaseCharacter = coreInfo.unicharString() 16 | if uppercaseCharacter is None: 17 | print("⚠️ Cannot determine character for: %s" % originalGlyphName) 18 | return 0 19 | else: 20 | lowercaseCharacter = uppercaseCharacter.lower() 21 | if uppercaseCharacter == lowercaseCharacter: 22 | print("🆗 %s: unchanged, cannot be lowercased." % originalGlyphName) 23 | return 0 24 | else: 25 | lowercaseCoreName = Glyphs.niceGlyphName(lowercaseCharacter) 26 | glyphNameParts[0] = lowercaseCoreName 27 | lowercaseGlyphName = ".".join(glyphNameParts) 28 | thisFont = thisGlyph.parent 29 | if thisFont.glyphs[lowercaseGlyphName]: 30 | print("❌ %s: cannot convert to %s, glyph already exists." % (originalGlyphName, lowercaseGlyphName)) 31 | return 0 32 | else: 33 | thisGlyph.name = lowercaseGlyphName 34 | thisGlyph.updateGlyphInfo() 35 | print( 36 | "✅ %s → %s" % ( 37 | originalGlyphName, 38 | thisGlyph.name if thisGlyph.name == lowercaseGlyphName else "%s → %s (updated glyph info)" % (lowercaseGlyphName, thisGlyph.name), 39 | ) 40 | ) 41 | return 1 42 | 43 | 44 | Glyphs.clearLog() # clears macro window log 45 | Font = Glyphs.font 46 | selectedGlyphs = [layer.parent for layer in Font.selectedLayers] 47 | countSelectedGlyphs = len(selectedGlyphs) 48 | convertedCount = 0 49 | print("Converting %i selected glyphs to lowercase:\n" % countSelectedGlyphs) 50 | 51 | Font.disableUpdateInterface() 52 | try: 53 | for thisGlyph in selectedGlyphs: 54 | convertedCount += lowercaseGlyphName(thisGlyph) 55 | except Exception as e: 56 | Glyphs.showMacroWindow() 57 | print("\n⚠️ Script Error:\n") 58 | import traceback 59 | print(traceback.format_exc()) 60 | print() 61 | raise e 62 | finally: 63 | Font.enableUpdateInterface() 64 | 65 | # Floating notification: 66 | Glyphs.showNotification( 67 | "%s: LC Conversion Finished" % (Font.familyName), 68 | "Of %i selected glyph%s, %i %s converted to lowercase. Details in Macro Window." % ( 69 | countSelectedGlyphs, 70 | "" if countSelectedGlyphs == 1 else "s", 71 | convertedCount, 72 | "was" if convertedCount == 1 else "were", 73 | ), 74 | ) 75 | 76 | print("\n%i glyph%s converted to lowercase.\nDone." % ( 77 | convertedCount, 78 | "" if convertedCount == 1 else "s", 79 | )) 80 | -------------------------------------------------------------------------------- /Build Glyphs/Build small letter SM, TEL.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Build small letter SM, TEL 2 | # -*- coding: utf-8 -*- 3 | from __future__ import division, print_function, unicode_literals 4 | __doc__ = """ 5 | Creates the glyphs: servicemark, telephone. 6 | """ 7 | 8 | from GlyphsApp import Glyphs, GSGlyph, GSComponent 9 | from mekkablue.geometry import transform, offsetLayer 10 | 11 | expansion = 5 12 | newGlyphs = { 13 | "servicemark": "SM", 14 | "telephone": "TEL" 15 | } 16 | 17 | thisFont = Glyphs.font # frontmost font 18 | selectedLayers = thisFont.selectedLayers # active layers of selected glyphs 19 | 20 | reference = thisFont.glyphs["M"] 21 | trademark = thisFont.glyphs["trademark"] 22 | 23 | thisFont.disableUpdateInterface() # suppresses UI updates in Font View 24 | try: 25 | for thisMaster in thisFont.masters: 26 | tmLayer = trademark.layers[thisMaster.id] 27 | smallscale = tmLayer.bounds.size.height - 2 * expansion 28 | largescale = reference.layers[thisMaster.id].bounds.size.height 29 | scale = smallscale / largescale 30 | upshift = tmLayer.bounds.origin.y + expansion 31 | 32 | measureLayer = tmLayer.copyDecomposedLayer() 33 | measureLayer.removeOverlap() 34 | distance = measureLayer.paths[1].bounds.origin.x - (measureLayer.paths[0].bounds.origin.x + measureLayer.paths[0].bounds.size.width) 35 | 36 | for newGlyphName in newGlyphs: 37 | newGlyph = thisFont.glyphs[newGlyphName] 38 | if not newGlyph: 39 | newGlyph = GSGlyph(newGlyphName) 40 | thisFont.glyphs.append(newGlyph) 41 | 42 | newGlyph.leftKerningGroup = trademark.leftKerningGroup 43 | newGlyph.rightKerningGroup = trademark.rightKerningGroup 44 | newLayer = newGlyph.layers[thisMaster.id] 45 | newLayer.clear() 46 | for i, letter in enumerate(newGlyphs[newGlyphName]): 47 | letterComp = GSComponent(letter) 48 | newLayer.components.append(letterComp) 49 | letterComp.disableAlignment = True 50 | scaleDown = transform(scale=scale).transformStruct() 51 | letterComp.applyTransform(scaleDown) 52 | if i > 0: 53 | prevComp = newLayer.components[i - 1] 54 | newOrigin = prevComp.bounds.origin.x + prevComp.bounds.size.width + distance + 2 * expansion 55 | currentOrigin = letterComp.bounds.origin.x 56 | shiftRight = transform(shiftX=(newOrigin - currentOrigin)).transformStruct() 57 | letterComp.applyTransform(shiftRight) 58 | 59 | newLayer.decomposeComponents() 60 | newLayer.anchors = None 61 | shiftUp = transform(shiftY=upshift).transformStruct() 62 | newLayer.applyTransform(shiftUp) 63 | offsetLayer(newLayer, expansion) 64 | newLayer.LSB = tmLayer.LSB 65 | newLayer.RSB = tmLayer.RSB 66 | except Exception as e: 67 | Glyphs.showMacroWindow() 68 | print("\n⚠️ Script Error:\n") 69 | import traceback 70 | print(traceback.format_exc()) 71 | print() 72 | raise e 73 | finally: 74 | thisFont.enableUpdateInterface() # re-enables UI updates in Font View 75 | --------------------------------------------------------------------------------