Skip to content

Instantly share code, notes, and snippets.

@ryanbugden
Last active January 9, 2024 02:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanbugden/8e1f9d3ea82ea7ff6b53a2c93df400ed to your computer and use it in GitHub Desktop.
Save ryanbugden/8e1f9d3ea82ea7ff6b53a2c93df400ed to your computer and use it in GitHub Desktop.
A simple window to manipulate unicode stuff across your font(s).
# menuTitle: Unicorn
import ezui
from mojo.extensions import setExtensionDefault, getExtensionDefault
from glyphNameFormatter.reader import u2n, u2U, U2u, n2u
EXTENSION_KEY = "com.ryanbugden.unicorn.settings"
class Unicorn(ezui.WindowController):
def build(self):
content = """
* TwoColumnForm @form1
> : Span:
> ( ) Selected Glyphs @radioButtons
> (X) Current Font
> ( ) All Fonts
---
* TwoColumnForm @form2
> : Auto:
> (Unicodes) @autoUniButton
> (Glyph Names) @autoNameButton
> ---
> : Double-Map:
> (All-Uppercase) @allUpperButton
> (All-Lowercase) @allLowerButton
> (Miscellaneous) @miscDblMapButton
> ---
> : Remove:
> (Misplaced Unicodes) @removeMisplacedButton
> (Secondary Unicodes) @removeSecondaryButton
> (All Unicodes) @removeAllButton
> ---
> : Mark Glyphs:
> * HorizontalStack @markHorizStack
>> * ColorWell @markColorWell
>> ({xmark}) @removeMarkButton
> (With Unicodes) @markUnicodedButton
> (Without Unicodes) @markEmptyButton
"""
image_button_width = 20
title_column_width = 90
item_column_width = 150
button_width = item_column_width
symbol_config = {
'scale' : 'medium',
'weight' : 'regular',
}
descriptionData = dict(
form1=dict(
titleColumnWidth=title_column_width,
itemColumnWidth=item_column_width
),
form2=dict(
titleColumnWidth=title_column_width,
itemColumnWidth=item_column_width
),
autoUniButton=dict(
width=button_width
),
autoNameButton=dict(
width=button_width
),
removeMisplacedButton=dict(
width=button_width
),
removeSecondaryButton=dict(
width=button_width
),
removeAllButton=dict(
width=button_width
),
allUpperButton=dict(
width=button_width
),
allLowerButton=dict(
width=button_width
),
miscDblMapButton=dict(
width=button_width
),
markHorizStack=dict(
distribution="equalSpacing",
width=button_width,
height=22,
),
removeMarkButton=dict(
symbolConfiguration=symbol_config,
drawBorder=True,
width=image_button_width
),
markEmptyButton=dict(
width=button_width,
),
markUnicodedButton=dict(
width = button_width,
),
)
self.w = ezui.EZPanel(
title="Unicorn",
content=content,
descriptionData=descriptionData,
controller=self
)
self.w.getNSWindow().setTitlebarAppearsTransparent_(True)
self.w.setItemValues(getExtensionDefault(EXTENSION_KEY, self.w.getItemValues()))
def started(self):
self.w.open()
def destroy(self):
setExtensionDefault(EXTENSION_KEY, self.w.getItemValues())
def report_changes(self, unicodes_before, g):
unicodes_after = g.unicodes
if unicodes_after != unicodes_before:
change_text = f"\t{g.font.info.familyName} {g.font.info.styleName}: {g.name} ({g.layer.name}):"
spaces = " " * (40 - len(change_text))
change_text += spaces
change_text += f"{unicodes_before} → {unicodes_after}"
return change_text
return None
def get_glyph_span(self):
a, f = AllFonts(), CurrentFont()
options = {
0: ([l[g_name] for g_name in f.selectedGlyphNames for l in f.layers if g_name in l.keys()]),
1: ([l[g_name] for l in f.layers for g_name in l.keys()]),
2: ([l[g_name] for f in a for l in f.layers for g_name in l.keys()])
}
return options[self.w.getItem('radioButtons').get()]
def autoUniButtonCallback(self, sender):
self.auto_unicodes(self.get_glyph_span())
def autoNameButtonCallback(self, sender):
self.auto_glyph_names(self.get_glyph_span())
def removeMisplacedButtonCallback(self, sender):
self.remove_misplaced(self.get_glyph_span())
def removeSecondaryButtonCallback(self, sender):
self.remove_secondary_unicodes(self.get_glyph_span())
def removeAllButtonCallback(self, sender):
self.remove_all_unicodes(self.get_glyph_span())
def allUpperButtonCallback(self, sender):
self.make_all_caps_or_lower(self.get_glyph_span(), method='upper')
def allLowerButtonCallback(self, sender):
self.make_all_caps_or_lower(self.get_glyph_span(), method='lower')
def miscDblMapButtonCallback(self, sender):
self.misc_double_map(self.get_glyph_span())
def markUnicodedButtonCallback(self, sender):
self.mark_color = tuple([round(val, 5) for val in self.w.getItem("markColorWell").get()])
self.mark_unicoded(self.get_glyph_span())
def markEmptyButtonCallback(self, sender):
self.mark_color = tuple([round(val, 5) for val in self.w.getItem("markColorWell").get()])
self.mark_empty(self.get_glyph_span())
def removeMarkButtonCallback(self, sender):
self.mark_color = tuple([round(val, 5) for val in self.w.getItem("markColorWell").get()])
self.remove_mark(self.get_glyph_span())
def auto_unicodes(self, list_of_glyphs):
changed = []
for g in list_of_glyphs:
unicodes_before = g.unicodes
g.autoUnicodes()
changes = self.report_changes(unicodes_before, g)
if changes:
changed.append(changes)
print(f"Unicorn: Automatically assigned the following unicodes:")
for change in sorted(changed):
print(change)
def auto_glyph_names(self, list_of_glyphs):
for g in list_of_glyphs:
try:
g.name = u2n(g.unicode)
except ValueError:
continue
except TypeError:
continue
def remove_misplaced(self, list_of_glyphs):
removed = []
for g in list_of_glyphs:
if n2u(g.name) == None and g.unicodes != ():
g.unicodes = ()
removed.append(g.name)
if removed:
print(f"Unicorn: Removed unicodes from {removed}")
else:
print(f"Unicorn: There are no extraneously assigned unicodes.")
def remove_secondary_unicodes(self, list_of_glyphs):
removed = []
for g in list_of_glyphs:
if len(g.unicodes) > 1:
single_unicode = g.unicodes[0]
g.unicodes = ()
g.unicode = single_unicode
removed.append(g.name)
if removed:
print(f"Unicorn: Removed secondary unicodes from {removed}")
else:
print(f"Unicorn: Did not have to remove any secondary unicodes from the selected set of glyphs.")
def remove_all_unicodes(self, list_of_glyphs):
removed = []
for g in list_of_glyphs:
if g.unicodes != ():
g.unicodes = ()
removed.append(g.name)
if removed:
print(f"Unicorn: Removed unicodes from {removed}")
else:
print(f"Unicorn: Did not have to remove any unicodes from the selected set of glyphs.")
def mark_unicoded(self, list_of_glyphs):
self.remove_mark(list_of_glyphs)
for g in list_of_glyphs:
if g.unicodes != ():
g.markColor = self.mark_color
def mark_empty(self, list_of_glyphs):
self.remove_mark(list_of_glyphs)
for g in list_of_glyphs:
if g.unicodes == ():
g.markColor = self.mark_color
def remove_mark(self, list_of_glyphs):
for g in list_of_glyphs:
if g.markColor == self.mark_color:
g.markColor = None
def make_all_caps_or_lower(self, list_of_glyphs, method='upper'):
exceptions = [
(("Germandbls", (7838, 223, 7838)), ("germandbls", (223, 223, 7838)))
]
changed = []
for g in list_of_glyphs:
unicodes_before = g.unicodes
g.unicodes = ()
base_uni = n2u(g.name)
lower_uni = U2u(base_uni)
upper_uni = u2U(base_uni)
# Manually adding exceptions
for (upper, lower) in exceptions:
if g.name == upper[0]:
base_uni = upper[1][0]
lower_uni = upper[1][1]
upper_uni = upper[1][2]
break
elif g.name == lower[0]:
base_uni = lower[1][0]
lower_uni = lower[1][1]
upper_uni = lower[1][2]
break
# Is neither lowercase nor uppercase
if base_uni == upper_uni and base_uni == lower_uni:
g.unicode = base_uni
# Is lowercase
elif base_uni == lower_uni:
if method == 'lower':
# Remove unicode from respective uppercase
upper_name = u2n(upper_uni)
if upper_name in g.layer.keys():
g.layer[upper_name].unicodes = ()
# Set unicodes
g.unicodes = (lower_uni, upper_uni)
# Is uppercase (more specifically, definitely not lowercase)
elif base_uni == upper_uni:
if method == 'upper':
# Remove unicode from respective lowercase
lower_name = u2n(lower_uni)
if lower_name in g.layer.keys():
g.layer[lower_name].unicodes = ()
# Set unicodes
g.unicodes = (upper_uni, lower_uni)
changes = self.report_changes(unicodes_before, g)
if changes: changed.append(changes)
print(f"Unicorn: Made font all-{method}case with the following unicode changes:")
for change in sorted(changed):
print(change)
def misc_double_map(self, list_of_glyphs):
misc_dbl = {
# From AFDKO: https://github.com/adobe-type-tools/afdko/blob/b955b771aacfac517b0f12a761073d4b1a2f9a88/python/afdko/makeotf.py#L208-L225
'space': (32, 2560),
'fraction': (8260, 8725),
# 'hyphen': (45, 173), # See: https://typedrawers.com/discussion/2046/special-dash-things-softhyphen-horizontalbar
'macron': (175, 713),
'mu': (181, 956),
'periodcentered': (183, 8729),
'Delta': (8710, 916),
'Omega': (8486, 937),
'Scedilla': (350, 63169),
'scedilla': (351, 63170),
'Tcommaaccent': (354, 538),
'tcommaaccent': (355, 539)
}
changed = []
for g in list_of_glyphs:
unicodes_before = g.unicodes
if g.name in misc_dbl.keys():
g.unicodes = misc_dbl[g.name]
# Remove unicodes from other glyphs that might have those unicodes.
for uni in misc_dbl[g.name]:
for other_gn in g.layer.keys():
if other_gn != g.name:
other_g = g.layer[other_gn]
other_unicodes_before = other_g.unicodes
if uni in other_g.unicodes:
other_g.unicodes = ()
changes = self.report_changes(other_unicodes_before, other_g)
if changes: changed.append(changes)
changes = self.report_changes(unicodes_before, g)
if changes: changed.append(changes)
print(f"Unicorn: Double-mapped unicodes with the following unicode changes:")
for change in sorted(changed):
print(change)
Unicorn()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment