Last active
April 30, 2021 20:02
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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