Skip to content

Instantly share code, notes, and snippets.

@connordavenport
Last active April 30, 2021 20:02
Show Gist options
  • Select an option

  • Save connordavenport/9c900b34bc0ec97b30d9fbe5f91acbf3 to your computer and use it in GitHub Desktop.

Select an option

Save connordavenport/9c900b34bc0ec97b30d9fbe5f91acbf3 to your computer and use it in GitHub Desktop.
A SFNT glyph copier. I want to stop using OTMaster so this replaces that tool. The name is random.
from AppKit import NSFilenamesPboardType, NSDragOperationCopy
from mojo.events import BaseEventObserver
import os
import vanilla
from fontParts.world import RGlyph
from fontTools.ttLib import TTFont
from fontTools.subset.cff import *
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.pens.ttGlyphPen import TTGlyphPen
import difflib
'''
a tool to replace OTMaster's glyph copier
'''
class ForumlaOne(BaseEventObserver):
def __init__(self):
self.L = 0 # left
self.T = 0 # top
self.W = 800 # width
self.H = 300 # height
self.p = 5 # padding
self.buttonHeight = 20
self.glyphList = []
self.isClose = []
self.newKey = None
title = f"ForumlaOne"
self.w = vanilla.Window((self.W, self.H), title)
self.w.sourceText = vanilla.TextBox((self.p,self.p,200,20),"Source Fonts:",sizeStyle="small")
self.w.destText = vanilla.TextBox((300 + (self.p * 2),self.p,200,20),"Destination Fonts:",sizeStyle="small")
self.w.glyphsText = vanilla.TextBox((600 + (self.p * 3),self.p,200,20),"Glyphs To Copy:",sizeStyle="small")
self.w.radioGroup = vanilla.RadioGroup((self.p, self.p, 200, -(self.p * 2 + self.buttonHeight)),
[],
callback=self.OTCallback)
self.w.sourceList = vanilla.List(
(self.p, self.p + 15, 300, -(self.p * 2 + self.buttonHeight)),
[],
columnDescriptions=[
{"title": "?", "width": 25}, # status
{"title": "baseName", "allowsSorting": True}], # files
drawFocusRing=False,
showColumnTitles=False,
allowsMultipleSelection=True,
enableDelete=True,
otherApplicationDropSettings = dict(
type=NSFilenamesPboardType,
operation=NSDragOperationCopy,
callback=self.dropCallback),
)
self.w.destList = vanilla.List(
(300 + (self.p * 2), self.p + 15, 300, -(self.p * 2 + self.buttonHeight)),
[],
columnDescriptions=[
{"title": "?", "width": 25}, # status
{"title": "baseName", "allowsSorting": True}], # files
drawFocusRing=False,
showColumnTitles=False,
allowsMultipleSelection=True,
enableDelete=True,
otherApplicationDropSettings = dict(
type=NSFilenamesPboardType,
operation=NSDragOperationCopy,
callback=self.dropCallback),
)
self.w.glyphInput = vanilla.EditText((600 + (self.p * 3), self.p + 15 , 180, -105),
placeholder='copy a space seperated list of glyphs here', callback=self.editTextCallback)
self.w.bringComponents = vanilla.CheckBox((600 + (self.p * 4), 200, 180, 20), "Bring Referenced Glyphs", sizeStyle="small")
self.w.bringComponents.getNSButton().setToolTip_("If glyph is component based, this will bring over all glyphs referenced. (TTF only)")
self.w.appendGlyphs = vanilla.CheckBox((600 + (self.p * 4), 220, 180, 20), "Append Glyphs", sizeStyle="small", value=True)
self.w.appendGlyphs.getNSButton().setToolTip_("If glyph is not in the destination font, it will be appended at the end.")
self.w.bringUnicode = vanilla.CheckBox((600 + (self.p * 4), 240, 180, 20), "Add Unicode", sizeStyle="small", value=True)
self.w.bringUnicode.getNSButton().setToolTip_("If glyph is not in the destination font and it is being appended, add the source unicode.")
self.w.compilebutton = vanilla.Button((self.p, -(self.p + self.buttonHeight), -self.p, self.buttonHeight),
"Drive", callback=self.compileButtonCallback)
self.w.compilebutton.enable(False)
self.w.open()
def editTextCallback(self,sender):
self.glyphList = [glyphName for glyphName in sender.get().split()]
def compileMap(self,list1,list2):
output = {}
for index, item in enumerate(list1):
try:
output[item] = list2[index]
except IndexError:
pass
return output
def isCloseSheetCallback(self):
self.sheet = vanilla.Sheet((200, 200), self.w)
self.sheet.header = vanilla.TextBox((self.p,self.p,200,20),"__ is not in the source font")
self.sheet.radioGroup = vanilla.RadioGroup((self.p, self.p + 40, 200-10, 120),[])
self.sheet.cancelButton = vanilla.Button((self.p, -(self.p + self.buttonHeight), ((200-15)/2), 20), "Cancel", callback=self.closeisCloseSheetCallback)
self.sheet.acceptButton = vanilla.Button(((self.p * 2) + ((200-15)/2), -(self.p + self.buttonHeight), ((200-15)/2), 20), "Accept", callback=self.getAcceptedGlyphCallback)
self.addFeatureObservers()
# self.sheet.open()
def closeisCloseSheetCallback(self, sender):
self.sheet.close()
del self.sheet
def getAcceptedGlyphCallback(self,sender):
self.newKey = self.isClose[self.sheet.radioGroup.get()]
self.closeisCloseSheetCallback(None)
def compileButtonCallback(self,sender):
for source, dest in self.compileMap(self.w.sourceList, self.w.destList).items():
s = TTFont(source["Files"])
d = TTFont(dest["Files"])
if s.sfntVersion == d.sfntVersion:
for key in self.glyphList:
if key not in s.getGlyphOrder():
self.isClose = difflib.get_close_matches(key, s.getGlyphOrder())
self.isCloseSheetCallback()
if hasattr(self.sheet, "radioGroup"):
delattr(self.sheet, "radioGroup")
if hasattr(self.sheet, "header"):
delattr(self.sheet, "header")
setattr(self.sheet, "header", vanilla.TextBox((self.p,self.p,200,20),f"{key} is not in the source font"))
setattr(self.sheet, "radioGroup", vanilla.RadioGroup((self.p, self.p + 40, 200-10, 120), self.isClose))
self.sheet.open()
key = self.newKey
else:
if self.w.appendGlyphs.get() == 0:
if key not in d.getGlyphOrder():
pass
else:
if s.sfntVersion == "OTTO":
if hasattr(s["CFF "].cff[0].Private, "Subrs"):
# source has subs on
if not hasattr(d["CFF "].cff[0].Private, "Subrs"):
# dest doesnt have subs
s["CFF "].desubroutinize()
else:
# for now we can remove all the subs
# add more to here later
s["CFF "].desubroutinize()
charString = s["CFF "].cff[0].CharStrings[key]
topDict = d["CFF "].cff[0]
cString = topDict.CharStrings
charStringsIndex = cString.charStringsIndex
'''
To Do:
copy over cff subroutines:
can you copy a glyph without the subroutines?
if dest font does not have subs but source does:
make new subs/globalsubs
copy over source subs/globalsubs
or
remove subs?
if dest font has subs but source does not:
just copy over and ignore subs
if source and dest both have subs:
WHO KNOWS
'''
if key in cString:
glyphID = cString.charStrings[key]
charStringsIndex.items[glyphID] = charString
else:
charStringsIndex.append(charString)
glyphID = len(topDict.charset)
cString.charStrings[key] = glyphID
topDict.charset.append(key)
else:
glyf = d["glyf"]
sg = s.getGlyphSet()[key]
glyf[key] = Glyph()
pen = TTGlyphPen(s.getGlyphSet())
sg.draw(pen)
glyf[key] = pen.glyph()
# add glyphs from components if not in list already
if self.w.bringComponents.get() == 1:
try:
comps = glyf[key].components
for cs in comps:
c = cs.getComponentInfo()[0]
if c not in self.glyphList:
self.glyphList.append(c)
except AttributeError:
pass
try:
glyf[key].program = sg._glyph.program
except AttributeError:
pass
destUnicodes = {u:n for t in d["cmap"].tables for u,n in t.cmap.items()}
sourceUnicodes = {n:u for t in s["cmap"].tables for u,n in t.cmap.items()}
if self.w.bringUnicode.get() == 1:
unicode = sourceUnicodes.get(key)
if unicode:
if key in d.getGlyphOrder():
pass
else:
if unicode in list(destUnicodes.keys()):
print(f"{key} unicode already in CMAP but glyphName isn't.")
else:
for table in d["cmap"].tables:
table.cmap[unicode] = key
d["hmtx"].metrics[key] = s["hmtx"].metrics[key]
s.close()
d.save(dest["Files"])
def dropCallback(self, sender, dropInfo):
if sender.getPosSize()[0] == 5:
so = "Source"
existingPaths = [ff['Files'] for ff in self.w.sourceList]
if sender.getPosSize()[0] == 310:
so = "Dest"
existingPaths = [ff['Files'] for ff in self.w.destList]
isProposal = dropInfo["isProposal"]
paths = dropInfo["data"]
paths = [path for path in paths if path not in existingPaths]
paths = [path for path in paths if os.path.splitext(path)[-1].lower() in ['.otf','.ttf'] or os.path.isdir(path)]
if not paths:
return False
if not isProposal:
# deactivate component option if CFF
if TTFont(paths[0]).sfntVersion == "OTTO":
self.w.bringComponents.enable(False)
for path in paths:
directory, ufoPath = os.path.split(path)
item = {}
item['?'] = '○'
item['Files'] = path
item['baseName'] = os.path.basename(path)
if so == "Source":
self.w.sourceList.append(item)
else:
self.w.destList.append(item)
self.w.compilebutton.enable(True)
return True
ForumlaOne()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment