From 96f269c4cc85c3cb6b4191775849a6721e76bc78 Mon Sep 17 00:00:00 2001 From: Daniel Spreadbury Date: Sat, 16 Feb 2019 23:07:42 +0000 Subject: [PATCH] Adding useful scripts for Glyphs font editor. --- scripts/glyphsapp/README.md | 14 ++ scripts/glyphsapp/generate_font_metadata.py | 98 ++++++++++++ scripts/glyphsapp/populate_ranges.py | 99 ++++++++++++ .../set_display_name_to_codepoint.py | 39 +++++ .../set_display_name_to_glyph_description.py | 38 +++++ .../set_display_name_to_glyph_name.py | 38 +++++ .../glyphsapp/set_metadata_and_category.py | 42 +++++ scripts/glyphsapp/smufl_glyphs.py | 145 ++++++++++++++++++ 8 files changed, 513 insertions(+) create mode 100644 scripts/glyphsapp/README.md create mode 100644 scripts/glyphsapp/generate_font_metadata.py create mode 100755 scripts/glyphsapp/populate_ranges.py create mode 100644 scripts/glyphsapp/set_display_name_to_codepoint.py create mode 100644 scripts/glyphsapp/set_display_name_to_glyph_description.py create mode 100644 scripts/glyphsapp/set_display_name_to_glyph_name.py create mode 100755 scripts/glyphsapp/set_metadata_and_category.py create mode 100755 scripts/glyphsapp/smufl_glyphs.py diff --git a/scripts/glyphsapp/README.md b/scripts/glyphsapp/README.md new file mode 100644 index 0000000..ac4aac0 --- /dev/null +++ b/scripts/glyphsapp/README.md @@ -0,0 +1,14 @@ +For these scripts to work, you'll need to put three SMuFL metadata files into the same folder as the scripts: + +- bravura_metadata.json +- glyphnames.json +- ranges.json + +**Important note: these scripts will modify your Glyphs project / font file so it's critical to have backups before running any operations. While I've tested the scripts here, I cannot guarantee that it will work in every single situation.** + +Now I've got the disclaimer out of the way, the first thing to run is the "Set metadata.." script. You should see all your glyphs coloured and categorised. If you then run the "Set Glyph names to SMuFL names" it should sort them into the categories. You can switch between SMuFL names, descriptions and codepoints using these three scripts. My understanding is that you should run the "Set Glyph names to codepoints" before doing a final export of your font file for maximum compatibility with various applications which expect the glyph names to correspond to the codepoint. + +Once the metadata is set on the glyphs, the "generate_font_metadata.py" script should work as expected. + +There's one more extra script which is called "Populate ranges". If you select one or more glyphs and run this script, it will create any other missing glyphs from that range as empty glyphs. You can also edit the script and uncomment or edit the DEFAULT_RANGES_TO_POPULATE array with the range you'd like to create. Running the script with no glyphs selected will create the default ranges from this list. + diff --git a/scripts/glyphsapp/generate_font_metadata.py b/scripts/glyphsapp/generate_font_metadata.py new file mode 100644 index 0000000..dc356e7 --- /dev/null +++ b/scripts/glyphsapp/generate_font_metadata.py @@ -0,0 +1,98 @@ +#MenuTitle: Generate SMuFL metadata JSON (Anchors + BBoxes) +# -*- coding: utf-8 -*- +# SMuFL metadata generator for Glyohs +# The script should be added to Glyphs scripts folder +# +# By default it will output the metadata file onto the user's desktop, but this can be changed +# by adjusting the value of the OUTPUT_DIR variable. +# +# Features currently supported: +# - engraving defaults +# - bounding boxes +# - anchors +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! + +import json +import os +from time import gmtime, strftime + +OUTPUT_DIR = os.path.join( os.path.expanduser("~"), "Desktop" ) + +currentFont = Glyphs.font + +# The values in the dictionary below should be manually edited +font_metadata = { + "fontName": currentFont.familyName, + "fontVersion": 1.12, + "engravingDefaults": { + "arrowShaftThickness": 0.16, + "barlineSeparation": 0.4, + "beamSpacing": 0.25, + "beamThickness": 0.5, + "bracketThickness": 0.5, + "dashedBarlineDashLength": 0.5, + "dashedBarlineGapLength": 0.25, + "dashedBarlineThickness": 0.16, + "hairpinThickness": 0.16, + "legerLineExtension": 0.4, + "legerLineThickness": 0.16, + "lyricLineThickness": 0.16, + "octaveLineThickness": 0.16, + "pedalLineThickness": 0.16, + "repeatBarlineDotSeparation": 0.16, + "repeatEndingLineThickness": 0.16, + "slurEndpointThickness": 0.1, + "slurMidpointThickness": 0.22, + "staffLineThickness": 0.13, + "stemThickness": 0.12, + "subBracketThickness": 0.16, + "textEnclosureThickness": 0.16, + "thickBarlineThickness": 0.5, + "thinBarlineThickness": 0.16, + "tieEndpointThickness": 0.1, + "tieMidpointThickness": 0.22, + "tupletBracketThickness": 0.16 + }, + "glyphsWithAnchors": {}, + "glyphBBoxes": {}, + "optionalGlyphs": {}} + + +metadata_filename = os.path.join(OUTPUT_DIR, "%s_metadata_%s.json" % ( currentFont.familyName, + strftime( "%Y%m%d_%H%M%S", gmtime() ) ) ) + +print "Writing metadata to: %s" % metadata_filename + +def to_cartesian(val): + return float(val)/250 + +for g in currentFont.glyphs: + if len(g.layers) != 1: + print g, len(g.layers) + + layer = g.layers[0] + + glyph_name = g.userData['name'] + font_metadata["glyphBBoxes"][glyph_name] = \ + {"bBoxSW": [ + to_cartesian(layer.bounds.origin.x), + to_cartesian(layer.bounds.origin.y) + ], + "bBoxNE": [ + to_cartesian(layer.bounds.origin.x + layer.bounds.size.width), + to_cartesian(layer.bounds.origin.y + layer.bounds.size.height) + ] + } + if len(layer.anchors) > 0: + font_metadata['glyphsWithAnchors'][glyph_name] = {} + + for anchor in layer.anchors: + font_metadata['glyphsWithAnchors'][glyph_name][anchor.name] = \ + [to_cartesian(anchor.position.x), to_cartesian(anchor.position.y)] + +with open(metadata_filename, 'w') as outfile: + json.dump(font_metadata, outfile, indent=True, sort_keys=True) + +print "Done..." \ No newline at end of file diff --git a/scripts/glyphsapp/populate_ranges.py b/scripts/glyphsapp/populate_ranges.py new file mode 100755 index 0000000..f743b0e --- /dev/null +++ b/scripts/glyphsapp/populate_ranges.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +#MenuTitle: Populate ranges +# -*- coding: utf-8 -*- +# +# If you have one or more glyphs selected in given ranges, it will ensure that +# all the rest of the glyphs from those ranges are created. +# +# Note: it will default to setting the display name of newly created glyphs to +# the codepoint. This can be easily remedied by running one of the "set display +# name scripts". +# +# If the script is invoked with no selection it will populate the default ranges +# that are uncommented below. +# +# The script should be added to Glyphs scripts folder and expects the following +# SMuFL and Bravura metadata files to be present in the same directory. +# +# - bravura_metadata.json +# - glyphnames.json +# - ranges.json +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! +# +__doc__=""" + +""" + +import sys +from smufl_glyphs import SMuFLFontSyncer, invoke_menu_item + +# Uncomment any ranges that you'd like to be populated when nothing is selected. +DEFAULT_RANGES_TO_POPULATE = [ + # u'articulation', + # u'articulationSupplement', + # u'barlines', + # u'barRepeats', + # u'beamedGroupsOfNotes', + # u'beamsAndSlurs', + # u'chordSymbols', + # u'clefs', + # u'clefsSupplement', + # u'commonOrnaments', + # u'dynamics', + # u'flags', + # u'holdsAndPauses', + # u'individualNotes', + # u'lyrics', + # u'miscellaneousSymbols', + # u'multiSegmentLines', + # u'noteheads', + # u'noteNameNoteheads', + # u'octaves', + # u'octavesSupplement', + # u'otherAccidentals', + # u'otherBaroqueOrnaments', + # u'precomposedTrillsAndMordents', + # u'repeats', + # u'rests', + # u'roundAndSquareNoteheads', + # u'shapeNoteNoteheads', + # u'shapeNoteNoteheadsSupplement', + # u'slashNoteheads', + # u'staffBracketsAndDividers', + # u'standardAccidentals12Edo', + # u'standardAccidentalsChordSymbols', + # u'staves', + # u'stems', + # u'stringTechniques', + # u'timeSignatures', + # u'timeSignaturesSupplement', + # u'tremolos', + # u'tuplets', + # u'vocalTechniques', +] + +currentFont = Glyphs.font + +selected_glyphs = currentFont.selection +if len(selected_glyphs) == 0: + ranges_to_populate = DEFAULT_RANGES_TO_POPULATE +else: + ranges_to_populate = [] + for g in selected_glyphs: + range_id = g.userData['smufl_range'] + if range_id is not None and range_id not in ranges_to_populate: + ranges_to_populate.append(range_id) + +if len(ranges_to_populate) == 0: + print("No ranges to populate") + sys.exit() + +smufl_font_syncer = SMuFLFontSyncer(currentFont) +currentFont.disableUpdateInterface() +smufl_font_syncer.populate_ranges(ranges_to_populate) +currentFont.enableUpdateInterface() + + + diff --git a/scripts/glyphsapp/set_display_name_to_codepoint.py b/scripts/glyphsapp/set_display_name_to_codepoint.py new file mode 100644 index 0000000..60e772b --- /dev/null +++ b/scripts/glyphsapp/set_display_name_to_codepoint.py @@ -0,0 +1,39 @@ +#MenuTitle: Set Glyph names to codepoints +# -*- coding: utf-8 -*- +# +# Set the display name of the selected glyphs to show the codepoint. +# If the script is invoked with no selection it will apply to all glyphs. +# +# NOTE: this should be run before exporting for maximum compatibility with apps +# that expect the glyphs to be named according to their codepoint. +# +# The script should be added to Glyphs scripts folder and expects the following +# SMuFL and Bravura metadata files to be present in the same directory. +# +# - bravura_metadata.json +# - glyphnames.json +# - ranges.json +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! +# +__doc__=""" +Set the display name of the glyph to the unicode codepoint +""" + +from smufl_glyphs import set_display_name_to, invoke_menu_item, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX + +currentFont = Glyphs.font + +selected_glyphs = currentFont.selection +if len(selected_glyphs) == 0: + glyphs_to_change = currentFont.glyphs +else: + glyphs_to_change = selected_glyphs + +currentFont.disableUpdateInterface() +set_display_name_to(glyphs_to_change, 'uniCodepoint') +currentFont.enableUpdateInterface() +if len(selected_glyphs) == 0: + print ("Deselecting...") + invoke_menu_item(Glyphs, EDIT_MENU, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX) \ No newline at end of file diff --git a/scripts/glyphsapp/set_display_name_to_glyph_description.py b/scripts/glyphsapp/set_display_name_to_glyph_description.py new file mode 100644 index 0000000..ffc6f83 --- /dev/null +++ b/scripts/glyphsapp/set_display_name_to_glyph_description.py @@ -0,0 +1,38 @@ +#MenuTitle: Set Glyph names to SMuFL descriptions +# -*- coding: utf-8 -*- +# +# Set the display name of the selected glyphs to show the SMuFL description +# +# If the script is invoked with no selection it will apply to all glyphs. +# +# The script should be added to Glyphs scripts folder and expects the following +# SMuFL and Bravura metadata files to be present in the same directory. +# +# - bravura_metadata.json +# - glyphnames.json +# - ranges.json +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! +# +__doc__=""" +Set the display name of the glyph to the description of the glyph +""" + +from smufl_glyphs import set_display_name_to, invoke_menu_item, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX + +currentFont = Glyphs.font + +selected_glyphs = currentFont.selection +if len(selected_glyphs) == 0: + glyphs_to_change = currentFont.glyphs +else: + glyphs_to_change = selected_glyphs + +currentFont.disableUpdateInterface() +set_display_name_to(glyphs_to_change, 'description') +currentFont.enableUpdateInterface() +# If there weren't any glyphs selected when this script was invoked, make sure there aren't any afterwards. +if len(selected_glyphs) == 0: + print ("Deselecting...") + invoke_menu_item(Glyphs, EDIT_MENU, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX) \ No newline at end of file diff --git a/scripts/glyphsapp/set_display_name_to_glyph_name.py b/scripts/glyphsapp/set_display_name_to_glyph_name.py new file mode 100644 index 0000000..c8e88d0 --- /dev/null +++ b/scripts/glyphsapp/set_display_name_to_glyph_name.py @@ -0,0 +1,38 @@ +#MenuTitle: Set Glyph names to SMuFL names +# -*- coding: utf-8 -*- +# +# Set the display name of the selected glyphs to show the SMuFL glyph name. +# +# NOTE: this should be run before exporting for maximum compatibility with apps +# that expect the glyphs to be named according to their codepoint. +# +# The script should be added to Glyphs scripts folder and expects the following +# SMuFL and Bravura metadata files to be present in the same directory. +# +# - bravura_metadata.json +# - glyphnames.json +# - ranges.json +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! +# +__doc__=""" +Set the display name of the glyph to the SMuFL name of the glyph +""" + +from smufl_glyphs import set_display_name_to, invoke_menu_item, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX + +currentFont = Glyphs.font + +selected_glyphs = currentFont.selection +if len(selected_glyphs) == 0: + glyphs_to_change = currentFont.glyphs +else: + glyphs_to_change = selected_glyphs + +currentFont.disableUpdateInterface() +set_display_name_to(glyphs_to_change, 'name') +currentFont.enableUpdateInterface() +if len(selected_glyphs) == 0: + print ("Deselecting...") + invoke_menu_item(Glyphs, EDIT_MENU, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX) diff --git a/scripts/glyphsapp/set_metadata_and_category.py b/scripts/glyphsapp/set_metadata_and_category.py new file mode 100755 index 0000000..f14066c --- /dev/null +++ b/scripts/glyphsapp/set_metadata_and_category.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +#MenuTitle: Set metadata on selected (or all) glyphs, including categorisation +# -*- coding: utf-8 -*- +# +# Set the metadata for all the selected glyphs in the font. +# If the script is invoked with no selection it will apply to all glyphs. +# +# The script should be added to Glyphs scripts folder and expects the following +# SMuFL and Bravura metadata files to be present in the same directory. +# +# - bravura_metadata.json +# - glyphnames.json +# - ranges.json +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# Use, distribute and edit this script as you wish! +# +__doc__=""" + +""" + +from smufl_glyphs import SMuFLFontSyncer, invoke_menu_item, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX + +BRAVURA_METADATA_FILENAME = "bravura_metadata.json" +GLYPHNAMES_FILENAME = "glyphnames.json" +RANGES_FILENAME = "ranges.json" + +currentFont = Glyphs.font + +selected_glyphs = currentFont.selection +if len(selected_glyphs) == 0: + glyphs_to_change = currentFont.glyphs +else: + glyphs_to_change = selected_glyphs + +smufl_font_syncer = SMuFLFontSyncer(currentFont, BRAVURA_METADATA_FILENAME, GLYPHNAMES_FILENAME, RANGES_FILENAME) +currentFont.disableUpdateInterface() +smufl_font_syncer.sync_metadata(glyphs_to_change) +currentFont.enableUpdateInterface() +# If there weren't any glyphs selected when this script was invoked, make sure there aren't any afterwards. +if len(selected_glyphs) == 0: + invoke_menu_item(Glyphs, EDIT_MENU, GLYPHS_EDIT_MENU_DESELECT_ALL_IDX) \ No newline at end of file diff --git a/scripts/glyphsapp/smufl_glyphs.py b/scripts/glyphsapp/smufl_glyphs.py new file mode 100755 index 0000000..fb3b2e7 --- /dev/null +++ b/scripts/glyphsapp/smufl_glyphs.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The script should be added to Glyphs scripts folder and is used by other scripts +# +# Written by Ben Timms - Steinberg Media Technologies GmbH 2018 +# +# Use, distribute and edit this script as you wish! +# -*- coding: utf-8 -*- +__doc__=""" + +""" + +import json +import pprint +import binascii + +if __name__ != '__main__': + from __main__ import * + +GLYPHS_NUMBER_OF_AVAILABLE_COLOURS = 12 + +GLYPHS_EDIT_MENU_SELECT_ALL_IDX = 8 +GLYPHS_EDIT_MENU_DESELECT_ALL_IDX = 9 + +def invoke_menu_item(glyphs_app, glyphs_menu, menu_index): + glyphs_app.menu[glyphs_menu].submenu().performActionForItemAtIndex_(menu_index) + +def formatUniGlyphName(codepoint): + uniGlyphName = "uni%s" % codepoint.upper() + # print(uniGlyphName) + return uniGlyphName + +def get_colour_for_category(category): + return abs(hash(category)) % GLYPHS_NUMBER_OF_AVAILABLE_COLOURS + +def from_cartesian(value): + return value * 250 + +def set_display_name_to(glyphs_to_change, display_name_key): + for glyph in glyphs_to_change: + # print (glyph) + availableUserData = glyph.userData.keys() + if availableUserData is not None and display_name_key in availableUserData: + glyph_display_name = glyph.userData[display_name_key] + try: + glyph.name = glyph_display_name + except NameError: + # NOTE: This only handles one clash - we would need recursion to + # handle multiple + alt_glyph_display_name = "%s (1)" % glyph_display_name + print("WARNING: Unable to set the glyph display name: %s as it already exists - falling back to %s" % (glyph_display_name, alt_glyph_display_name)) + glyph.name = alt_glyph_display_name + else: + print ("WARNING: Display name key %s is not in glyph %s (Available keys: %s)" % (display_name_key, glyph, availableUserData)) + +class SMuFLFontSyncer(object): + + glyph_data_by_codepoint = {} + + def __init__(self, glyphs_font, + bravura_metadata_filename="bravura_metadata.json", + glyphnames_filename="glyphnames.json", + ranges_filename="ranges.json"): + self.font = glyphs_font + with open(bravura_metadata_filename, "r") as bravura_metadata_fh: + self.bravura_metadata = json.load(bravura_metadata_fh) + with open(glyphnames_filename, "r") as glyphnames_fh: + self.glyphnames = json.load(glyphnames_fh) + with open(ranges_filename, "r") as ranges_fh: + self.ranges_data = json.load(ranges_fh) + + self._populate_glyph_data_by_codepoint() + + def _populate_glyph_data_by_codepoint(self): + """ Create a local cache of glyph and range data indexed by codepoint """ + for smufl_glyph_name, glyph_data in self.glyphnames.items(): + self.glyph_data_by_codepoint[glyph_data['codepoint'][2:]] = { + 'description': glyph_data['description'], + 'name': smufl_glyph_name} + # Get the range and merge that in. + for range_id, range_data in self.ranges_data.items(): + for glyphname_in_range in range_data['glyphs']: + codepoint = self.glyphnames[glyphname_in_range]['codepoint'][2:] + self.glyph_data_by_codepoint[codepoint]['range_id'] = range_id + self.glyph_data_by_codepoint[codepoint]['range_description'] = range_data['description'] + + def sync_metadata(self, glyphs): + """ Iterate over the provided glyphs, ensuring the metadata is set """ + for g in glyphs: + if g.unicode not in self.glyph_data_by_codepoint: + print ("WARNING: No metadata for %s" % g.unicode) + continue + + glyph_data = self.glyph_data_by_codepoint[g.unicode] + glyph_data['codepoint'] = g.unicode + self._set_glyph_metadata(g, glyph_data ) + + def _set_glyph_metadata(self, glyph, glyph_data): + """ An internal method to set the metadata on the supplied glyph """ + glyph.storeCategory = True + glyph.category = glyph_data['range_description'] + glyph.color = get_colour_for_category(glyph_data['range_description']) + glyph.unicode = glyph_data['codepoint'] + glyph.userData['description'] = glyph_data['description'] + glyph.userData['name'] = glyph_data['name'] + glyph.userData['uniCodepoint'] = formatUniGlyphName(glyph.unicode) + glyph.userData['codepoint'] = glyph.unicode + glyph.userData['smufl_range'] = glyph_data['range_id'] + return glyph + + def populate_ranges(self, ranges_to_populate): + # print("Populating ranges: %s" % ranges_to_populate) + for range_id in ranges_to_populate: + self._populate_range(range_id) + + def _populate_range(self, range_id): + print("Populating range: %s" % range_id) + range_data = self.ranges_data[range_id] + + for smufl_glyph_name in range_data['glyphs']: + codepoint = self.glyphnames[smufl_glyph_name]['codepoint'][2:] + # print ("SMuFL Glyph name %s - Codepoint: %s" % (smufl_glyph_name, codepoint)) + glyph = self._get_or_create_glyph(codepoint) + glyph_description = self.glyphnames[smufl_glyph_name]['description'] + glyph_data = { + 'name': smufl_glyph_name, + 'codepoint': codepoint, + 'description': glyph_description, + 'range_id': range_id, + 'range_description': range_data['description'], + } + self._set_glyph_metadata(glyph, glyph_data) + # print ("Glyph: %s" % (glyph)) + # print ("Unicode: %s" % (glyph.unicode)) + + def _get_or_create_glyph(self, codepoint): + # Look for an existing glyph + glyph = self.font.glyphs[codepoint] + + if glyph is None: + glyph = GSGlyph(codepoint) + self.font.glyphs.append(glyph) + return glyph + + \ No newline at end of file